# Billing ITGroup Reseller API > Документация интеграции собственного биллинга реселлера. HTTP 202 не подтверждает завершение операции в реестре. API base URL: https://api.b.websoft.kz/api/reseller/v1 ## Полный комплект - [Полный текст руководств и всех операций](/llms-full.txt) - [OpenAPI 3.1 JSON](/openapi.json) - [OpenAPI 3.1 YAML](/reseller-api-openapi.yaml) - [Перечень скачиваемых исходников](/sources.json) ## Руководства Все руководства и все операции приведены ниже без сокращений. Нормативный машинный контракт со всеми components: /openapi.json или /reseller-api-openapi.yaml. Не выводите недокументированное поведение из примеров. --- SOURCE: /guide/concepts.md # Модель данных и расчётов ## Владелец данных API-ключ однозначно определяет реселлера и окружение. Передача чужого `provider_uuid`, UUID клиента или другого Host не переключает контекст. UUID не является доказательством права доступа. Чужой объект обычно скрывается как отсутствующий; не интерпретируйте каждый `404` как удаление объекта в реестре. | Режим | Как организовать интеграцию | | --- | --- | | `managed` | Создавать отдельных клиентов и передавать их `customer_uuid` в клиентозависимых операциях. Контакты домена должны принадлежать тому же клиенту. | | `aggregate` | Все услуги принадлежат клиенту, назначенному оператором для реселлера. Обычно `customer_uuid` опускается. Создание клиентов через API запрещено. Собственных конечных покупателей учитывать в своём биллинге. | Это режим учёта услуг, а не возможность выдать ключ конечному покупателю. Реселлер обязан проверять права своего пользователя до вызова API. ## Идентификаторы | Поле | Назначение | | --- | --- | | `customer_uuid` | Клиент внутри платформы, не UUID учётной записи пользователя. | | Contact `uuid` | Профиль доменного контакта. Это не EPP contact ID реестра. | | Offering `uuid` | Предложение в каталоге. Не подставлять вместо UUID зоны. | | Catalog `resource_uuid` | Ресурс предложения, например доменная зона; передаётся в quote. | | Order `uuid` | Заказ, в том числе асинхронная регистрация. | | `service_uuid` | Общая услуга биллинга. | | Domain `uuid`, `domain_uuid`, `domain_service_uuid` | Идентификатор доменного объекта, используемый в `/domains/{domainUuid}` и в payload quote для продления. Не равен UUID общей услуги. | | Operation `uuid` | Асинхронная операция управления, читается через `/operations/{operationUuid}`. | | `external_id` | Идентификатор соответствующего объекта в вашем биллинге. Заказ и услуга имеют разные external_id. | Сохраняйте соответствия UUID, а не пытайтесь вычислить их из имени домена. `external_id` уникален в своей области объектов реселлера и окружения; он помогает восстановить связь, но не заменяет `Idempotency-Key`. Клиенты, заказы и домены поддерживают поиск по `external_id` согласно справочнику. Не предполагайте такой фильтр у каждого списка: например, для контактов сохранение UUID обязательно. В managed список контактов требует `customer_uuid`. ## Деньги и quote Денежные поля с суффиксом `_minor` содержат целое число минимальных денежных единиц. Для KZT `125000` означает 1 250,00 тенге. Валюта передаётся отдельным кодом. Не рассчитывайте списания через `float` и не считайте, что у всех валют одинаковое число десятичных знаков. Каталог определяет доступность предложения, операций и периодов. Финальная сумма операции берётся из `POST /quotes`, а не из устаревшего локального прайса. Quote привязан к реселлеру, окружению, ключу и клиенту; нельзя создавать его одним ключом, а использовать другим. Срок действия по умолчанию 15 минут, фактический срок смотрите в ответе. Quote одноразовый. Суммы `unit_price_minor`, `subtotal_minor` и `total_minor` в публичном ответе quote являются розничными. Оптовая сумма резерва рассчитывается по правилам реселлера и может отличаться от `total_minor`; отдельная оптовая цена в этом ответе не возвращается. Финансовый результат сверяйте через закупочный баланс и ledger, не приравнивайте розничную сумму к оптовому списанию. Валюта всей котировки выбирается по первой строке `items` либо настройке реселлера: передавайте одинаковую валюту во всех строках. Для регистрации используется `operation: register`, для продления `renew`, для переноса `transfer`, для восстановления `restore`. Восстановление имеет отдельную цену: нельзя подставлять цену продления, если тариф восстановления не настроен. Реселлер с собственным биллингом использует согласованный с оператором `checkout_mode: external`. При платных live-операциях платформа резервирует средства оптового prepaid-баланса. Оплату вашего конечного покупателя и ваш розничный чек ведёт ваш биллинг. Подключение внешней платёжной системы этим API не производится. | Исход | Что делать в своём биллинге | | --- | --- | | Операция подтверждена | Зафиксировать оказание услуги; сверить сумму и итоговые данные. | | Подтверждённый отказ или ошибка до отправки | Проверить отказ и освобождение резерва; отдельно применить свою политику возврата покупателю. | | Неизвестный результат после отправки | Не создавать повторную покупку. Сохранить ожидание/ручную проверку: резерв может оставаться удержанным. | Ни `201`, ни `202` не являются доказательством окончательного списания и оказания услуги. Баланс и ledger доступны только live. Не обещайте покупателю автоматический возврат на банковскую карту на основании ошибки регистрации: это отдельный процесс вашего биллинга. ## Время, списки и состояния Метки времени ответов содержат ISO 8601 со смещением часового пояса; nullable-поля могут быть `null`. Календарная дата `profile.birth_date` возвращается отдельно в формате `YYYY-MM-DD`, без времени и смещения. Сравнивайте моменты времени, а не строки. Часовой пояс клиента не меняет момент окончания регистрации. Списки используют курсорную пагинацию. Передавайте полученный `next_cursor` как непрозрачную строку, сохраняя фильтры. Не используйте `page=2` и не вычисляйте курсор самостоятельно. `per_page` по умолчанию 25, максимум 100; конкретная оболочка `meta`/`links` приведена у метода. Отсутствие следующего курсора завершает обход, но не гарантирует согласованный снимок при параллельных изменениях. У домена раздельны состояние услуги, состояние регистрации и статусы реестра. Подтверждение личности не означает подтверждение образовательной лицензии. Перенос может долго оставаться pending. В локальной модели не сводите всё к одному флагу `active`. --- SOURCE: /guide/domain-management.md # Управление доменом Сначала получите `GET /domains/{domainUuid}` и `GET /domains/{domainUuid}/capabilities`. Capabilities описывают возможности драйвера, но не гарантируют достаточный баланс, scope или допустимое состояние реестра. Во всех изменяющих запросах требуется `Idempotency-Key`; для операций управления также `reason` длиной 3..1000 символов. ## Асинхронная операция Обычно изменяющий запрос возвращает HTTP `202`: ```json { "data": { "uuid": "66666666-6666-4666-8666-666666666666", "domain_uuid": "55555555-5555-4555-8555-555555555555", "action": "renew", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T09:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T09:00:00+00:00" } } ``` Читайте `GET /operations/{operationUuid}`. История доступна через `GET /domains/{domainUuid}/operations`, включая историю удалённого домена. Оба метода требуют `domains.read`. Повтор первоначального POST с прежним ключом воспроизводит первоначальный ответ, а не текущий прогресс. | Статус | Обработка | | --- | --- | | `pending`, `processing` | Продолжать ожидание с ограниченной частотой. | | `completed` | Прочитать итоговый Domain; не вычислять срок регистрации самостоятельно. После удаления обычный GET домена может вернуть `404`: результат и история операции остаются отдельными источниками состояния. | | `failed` | Проверить `error.code` и причину; новая попытка возможна только после устранения причины и подтверждённого исхода. | | `uncertain` | Команда могла выполниться. Не отправлять её заново и не освобождать свой резерв автоматически. | | `cancelled` или неизвестный статус | Не считать успехом. Сохранить состояние для сверки; универсального метода отмены операции нет. | `next_check_at` обозначает план серверной проверки, а не гарантированный срок завершения. При `failed` поле `completed_at` может оставаться `null`; пустой `result` сериализуется как `[]`, а заполненный обычно как объект. `uncertain` также используется при ожидании завершения переноса или восстановления в реестре, не только после потери ответа. Неизвестный результат может потребовать помощи оператора, особенно при изменении контактов, приватности или получении auth code. ## Продление 1. Получите срок домена и `renewal_available_at` из capabilities. 2. Выпустите quote с `operation: renew` и `payload.domain_service_uuid` равным UUID домена. 3. Отправьте `POST /domains/{domainUuid}/renew` с UUID quote и причиной. 4. Дождитесь `completed` и перечитайте `expires_at`. ```json { "customer_uuid":"11111111-1111-4111-8111-111111111111", "items":[{ "resource_type":"domain_zone", "resource_uuid":"22222222-2222-4222-8222-222222222222", "operation":"renew", "period_unit":"year", "period_count":1, "quantity":1, "currency":"KZT", "payload":{"domain_service_uuid":"55555555-5555-4555-8555-555555555555"} }] } ``` Тело запроса продления: ```json {"quote_uuid":"44444444-4444-4444-8444-444444444444","reason":"Продление по оплаченному заказу customer-order-1002"} ``` Период задаётся целыми годами. Максимальный суммарный срок учитывает уже оставшееся время и не может превышать 10 лет; доступные периоды дополнительно ограничены зоной и предложением. После регистрации должно пройти не менее 24 часов. Указание `period_count: 10` не означает, что десять лет разрешены любому уже действующему домену. Реестр может накладывать дополнительные ограничения. ## Автопродление `PUT /domains/{domainUuid}/auto-renew`: ```json {"enabled":true,"reason":"Согласие владельца на ежегодное продление по текущему тарифу"} ``` Ответ `200` сохраняет настройку и не продлевает домен немедленно. API-автопродление выключено по умолчанию, работает для external checkout и использует период один год по текущей цене. Проверяйте его флаг через `data.auto_renew` в capabilities: одноимённый флаг общей услуги `/services` относится к другому механизму. Планировщик ежечасно выбирает зарегистрированные домены с окончанием не позднее чем через семь дней, включая уже просроченные; допустимость продления дополнительно проверяется политикой домена и реестром. Это не SLA выполнения реестром. Следите за `domain.auto_renew.failed` и `domain.operation.updated`: недостаток средств, отзыв ключа или отсутствие цены требуют вмешательства. Выключение `enabled: false` не отменяет уже принятую операцию. При ротации/замене доступа проверьте работоспособность автопродления с оператором. ## Входящий перенос Для домена, которого ещё нет среди ваших услуг, получите quote `operation: transfer`, `period_unit: year`, `period_count: 1`, `quantity: 1`. В payload укажите `domain_name` в ASCII/Punycode, `external_id` и все четыре `contact_uuids`, как при регистрации. Затем: `POST /domains/transfers`: ```json { "quote_uuid":"44444444-4444-4444-8444-444444444444", "customer_uuid":"11111111-1111-4111-8111-111111111111", "auth_code":"REPLACE_WITH_REAL_TRANSFER_CODE", "reason":"Входящий перенос по заявке владельца" } ``` В aggregate `customer_uuid` опускается. Ответ `202` содержит операцию и `domain_uuid`. При переносе уже существующего локального домена используется `POST /domains/{domainUuid}/transfer`, а quote привязывается через `payload.domain_service_uuid`. Перенос может ожидать подтверждения несколько дней по правилам реестра. Для проверки и управления доступны `POST /transfer/query`, `/transfer/approve`, `/transfer/reject`, `/transfer/cancel` относительно домена с телом `{"reason":"Причина действия владельца"}`. Не путайте отмену переноса с отменой любой асинхронной задачи. Один год в quote является единицей тарификации переноса, а не обещанием прибавить год во всех реестрах. NS из quote входящего переноса не применяются: при необходимости измените их отдельной операцией после завершения. ## Исходящий перенос и auth code При согласии владельца снимите `clientTransferProhibited`, если это допустимо, и вызовите `POST /domains/{domainUuid}/auth-code`. Секрет появляется не в исходном `202`, а в `GET /operations/{operationUuid}` как `data.result.auth_code`, только если ключ имеет `domains.auth-code` и `domains.read`. Не записывайте auth code в логи, аналитику, тикеты или общий кэш. Ответы чтения операций имеют `Cache-Control: no-store`. Запрос на перенос подаёт принимающий регистратор; отдельного публичного метода «перенести к произвольному регистратору» здесь нет. ## Восстановление удалённого домена Возможность восстановления определяется реестром. Quote использует `operation: restore`, один год, количество один и `payload.domain_service_uuid`; для своего soft-deleted домена этот сценарий предусмотрен. Нужен отдельный настроенный тариф восстановления. `POST /domains/{domainUuid}/restore`: ```json { "quote_uuid":"44444444-4444-4444-8444-444444444444", "restore_auth":"REPLACE_WITH_REGISTRY_VALUE", "reason":"Восстановление по подтверждённой заявке владельца" } ``` Обязательно хотя бы одно непустое `restore_auth` или `restore_password`; нужное поле уточняется для регистратора. Не угадывайте секрет и не считайте срок доступности восстановления одинаковым для всех зон. Результат `202` отслеживается как обычная операция. ## NS, контакты и блокировки Все следующие примеры являются телами запросов относительно `/domains/{domainUuid}` и возвращают `202`. | Метод | Тело | | --- | --- | | `PUT /nameservers` | `{"nameservers":[{"hostname":"ns1.example.net"},{"hostname":"ns2.example.net"}],"reason":"Смена DNS-провайдера"}` | | `PUT /contacts` | `{"contacts":{"owner":"33333333-3333-4333-8333-333333333333","admin":"33333333-3333-4333-8333-333333333333","tech":"33333333-3333-4333-8333-333333333333","billing":"33333333-3333-4333-8333-333333333333"},"reason":"Смена контактных ролей"}` | | `PUT /statuses` | `{"add":["clientTransferProhibited"],"remove":[],"reason":"Запрет переноса владельцем"}` | | `PUT /whois-privacy` | `{"enabled":true,"hidden_fields":["address","phone","email"],"reason":"Защита контактных данных"}` | | `POST /sync` | `{"reason":"Сверка с состоянием реестра"}` | NS заменяются полным набором из 2..6 уникальных имён; optional `ipv4`/`ipv6` должны соответствовать семейству IP. Имена нормализуются, различие регистра не создаёт разные NS. Контактные роли передаются полностью. Изменение профиля через `PATCH /contacts/{contactUuid}` и назначение роли через `PUT /domains/{domainUuid}/contacts` являются разными действиями. PATCH контакта требует обязательных полей полной формы, а не произвольного частичного объекта. Не обещайте немедленное обновление всех реестровых объектов от локального редактирования контакта. Разрешены только `clientTransferProhibited`, `clientUpdateProhibited`, `clientDeleteProhibited`, `clientRenewProhibited`, `clientHold`. Один статус нельзя добавлять и удалять одновременно. `serverHold` и другие server-статусы этим методом не снимаются. Приватность зависит от зоны и регистратора; список полей: `name`, `organization`, `address`, `phone`, `fax`, `email`. ## Собственные NS: glue hosts | Метод | Тело или query | | --- | --- | | `POST /hosts` | `{"hostname":"ns1.example.kz","ipv4":"192.0.2.10","ipv6":"2001:db8::10","reason":"Создание собственного NS"}` | | `GET /hosts` | `?hostname=ns1.example.kz` | | `PUT /hosts` | `{"hostname":"ns1.example.kz","add_addresses":["192.0.2.11"],"remove_addresses":["192.0.2.10"],"reason":"Обновление адреса NS"}` | | `DELETE /hosts` | `{"hostname":"ns1.example.kz","reason":"Удаление неиспользуемого NS"}` | Разрешены только имена, подчинённые этому домену. При создании нужен хотя бы один IP; при изменении не более 10 адресов в каждом массиве. Эквивалентные формы IPv6 также считаются одним адресом при проверке пересечений. GET возвращает сведения об одном host, а не список DNS-записей или всех NS реестра. ## Удаление `DELETE /domains/{domainUuid}`: ```json {"confirm_domain":"example.kz","reason":"Удаление по документированному запросу владельца"} ``` `confirm_domain` должен точно совпадать с `domain_name` из GET, включая форму IDN. Получите явное согласие владельца в своём интерфейсе. HTTP `202` не означает немедленное освобождение имени; последствия и возможность восстановления определяются реестром. Это не удаление только локальной записи биллинга. ## Подтверждение владельца ЭЦП 1. Получите `GET /domains/{domainUuid}/registrant-verification/payload` со scope `domains.verify`. 2. Передайте точный `signable_payload` в NCALayer с возвращёнными параметрами. Не пересобирайте документ из snapshot. 3. Подпишите действующей ЭЦП надлежащего владельца/уполномоченного подписанта. Закрытый ключ и пароль не отправляются в API. 4. Отправьте `POST /domains/{domainUuid}/registrant-verification` с новым идемпотентным ключом. ```json { "type":"eds", "eds_provider":"ncalayer", "payload_snapshot":{"REPLACE":"exact object returned by payload endpoint"}, "signable_payload":"REPLACE_WITH_EXACT_RETURNED_STRING", "signature":"REPLACE_WITH_BASE64_DER_CMS", "signature_options":{ "method":"kz.gov.pki.knca.basics.sign", "format":"cms", "decode":false, "encapsulate":false, "digested":false, "timestamp_applied":true, "cms_type":"CMS Detached", "cades_profile":"CAdES-T" } } ``` Это шаблон связывания полей, а не готовая подпись: `payload_snapshot`, `signable_payload` и `signature_options` берутся из свежего ответа без изменений; `signature` является результатом подписания. Требуется detached CAdES-T с меткой времени, Base64 DER CMS без PEM. Проверяйте `expires_at`, обычно payload действует 15 минут. Ответ `201` содержит Verification, не Operation. Прогресс проверяется через Domain и событие `domain.verification.updated`; отдельного GET verification по UUID нет. При принятой pending-заявке повторное подписание не требуется. Успешная ЭЦП не гарантирует прохождение независимой проверки образовательной лицензии `.edu.kz`; её статус проверяется отдельно. --- SOURCE: /guide/examples.md # Исполняемые примеры Примеры не зависят от Laravel и не содержат действующих ключей. У каждого запроса в справочнике, включая варианты тела, есть вкладки с примерами на семи языках. Все они генерируются из одних параметров OpenAPI и передают одинаковые метод, query, заголовки и JSON. ## Языки запросов | Вкладка | Требования и HTTP-клиент | | --- | --- | | cURL | CLI curl с поддержкой `--fail-with-body` | | PHP 8+ | PHP **8.0 или новее**, расширения `curl` и `json`; `strict_types`, типизированный `CurlHandle`, без Composer-зависимостей | | JavaScript / Node.js | Node.js 22+, встроенный `fetch`; файл `.mjs`, не браузер | | Python | Python 3.10+, стандартный `urllib.request`, без pip-зависимостей | | Go | Go 1.22+, стандартный `net/http`; файл `main.go` | | Java | Java 17+, стандартный `java.net.http.HttpClient`; файл `RequestExample.java` | | C# / .NET | .NET 8+, консольный проект, стандартные `HttpClient` и `System.Text.Json`; файл `Program.cs` | Во всех серверных примерах настройте `RESELLER_API_TOKEN`, при необходимости `API_BASE_URL`, а для записи обязательный **заранее сохранённый** `IDEMPOTENCY_KEY`. UUID пути можно заменить прямо в примере либо задать переменной вроде `DOMAIN_UUID`. Демонстрационные UUID не являются действующими ресурсами. Не передавайте Bearer-ключ в браузер. Примеры выполняют один вызов без собственного цикла повторов. Они ограничивают время запроса/ожидания, не следуют перенаправлениям с секретом, сохраняют `X-Request-ID` и `Retry-After`, различают HTTP-ошибку и сетевой сбой. `204` обрабатывается без JSON-декодирования. При `429`, `5xx` или timeout применяйте [правила надёжности](reliability.md), а не запускайте новую покупку новым ключом. В PHP, Node.js, Python, Go и C# декодированный ответ доступен в переменной `data`/`$data`. Java SE не включает объектный JSON-маппер: пример сохраняет `responseBody`, который следует передать JSON-библиотеке вашего проекта. Полные ответы с секретами и персональными данными намеренно не выводятся в консоль. Код `202` означает только приём операции, не её завершение. В Java и C# для реального приложения переиспользуйте HTTP-клиент; показанный запуск является самостоятельным консольным примером. JSON и ключ сохраняйте до отправки, если реализуете восстановление после сбоя. Переключение языка не является способом повторить потерянный запрос: используйте исходные байты тела и ключ из своего журнала. Официальная документация используемых клиентов: [PHP cURL](https://www.php.net/manual/en/function.curl-setopt-array.php), [Node.js fetch](https://nodejs.org/api/globals.html#fetch), [Python urllib](https://docs.python.org/3/library/urllib.request.html), [Go net/http](https://pkg.go.dev/net/http), [Java HttpClient](https://docs.oracle.com/en/java/javase/17/docs/api/java.net.http/java/net/http/HttpClient.html), [.NET HttpClient](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=net-8.0). Полноценный возобновляемый учебный сценарий ниже требует Node.js 22+. Он дополняет отдельные примеры запросов, а не заменяет их. ## Файлы - [transport.mjs](/examples/transport.mjs): серверный HTTP-клиент, ограниченные повторы, Retry-After и чтение операций. - [register.mjs](/examples/register.mjs): возобновляемый сценарий регистрации только в test. - [registration-input.json](/examples/registration-input.json): входные данные сценария. - [transport.test.mjs](/examples/transport.test.mjs): тесты сетевых отказов без обращения к API. - [register.test.mjs](/examples/register.test.mjs): проверки сценария регистрации на локальном HTTP mock-сервере, без обращения к платформе или реестру. ## Проверить примеры без внешнего API В каталоге с файлами: ```bash node --test *.test.mjs ``` Транспортные тесты подменяют fetch и проверяют сохранение тела и ключа при сетевом сбое, ожидание 429, ограничение повторов 502, отказ от автоматического повторения 409/422, защиту URL и остановку при `uncertain`. Тесты runner запускают сценарий на локальном mock-сервере: managed с возобновлением сохранённого результата без повторных записей, aggregate без создания покупателя и без customer_uuid, превышение согласованной суммы, запрет live-ключа и запрет использования состояния другим credential. Это не тест доступности конкретного регистратора и не проверка валидации PHP-контроллеров. ## Выполнить сценарий в sandbox Сначала получите контекст, каталог и согласованные sandbox-данные. В `registration-input.json` замените `zone_uuid` на `resource_uuid` зоны из каталога, а также `contact_uuid`, имя домена и NS; установите собственные external_id и допустимую розничную сумму `max_total_minor`. Это локальные параметры примера, не отдельные поля тела API-запроса. Вместо существующего `contact_uuid` можно указать полный объект `contact`; в managed вместо создания клиента через `customer` можно задать существующий `customer_uuid`. В aggregate клиент назначается платформой, пример не отправляет customer_uuid ни в одном шаге. ```bash export RESELLER_API_BASE='https://api.b.websoft.kz/api/reseller/v1' export RESELLER_API_TOKEN='rsl_test_REPLACE_KEY.REPLACE_SECRET' node register.mjs registration-input.json /private/reseller-example/order-1001 --execute-test ``` Адрес API в опубликованном примере подставляется порталом из `DOCS_API_URL`; в исходнике используется шаблон, не готовый URL. Проверьте адрес перед передачей ключа. Скрипт отказывается от live-ключей и дополнительно проверяет окружение через `/context`. Флаг означает разрешение тестовых записей: sandbox всё равно может выполнять команды в тестовом реестре. Проверку доступности имени выполните заранее; она не гарантирует регистрацию. Перед каждой отправкой сохраняются ключ, метод, путь и точное тело. После ответа сохраняется результат шага. Повтор запуска с тем же файлом, директорией и credential использует подтверждённые результаты без повторных записей. Если у сохранённого запроса нет ответа, автоматическое возобновление по умолчанию запрещено. Сначала согласуйте с оператором фактический TTL HTTP-idempotency: `/context` его не возвращает. Для допустимого повтора задайте `RESELLER_REPLAY_WINDOW_SECONDS` как целое положительное число секунд строго меньше серверного TTL, с запасом на время отправки и возможное расхождение часов. Возраст намерения отсчитывается от его первоначального сохранения. При неизвестном TTL или истёкшем окне сначала сверяйте UUID/external_id с оператором. Для другого заказа используется другая директория и другие external_id. `state.json` содержит персональные данные и ответы, поэтому каталог должен находиться вне webroot, иметь ограниченные права и не попадать в Git или публичные backups. Встроенные права 0700/0600 применяются при создании; права уже существующей директории проверьте сами. Секрет ключа в state не сохраняется. `run.lock` предотвращает параллельные запуски. После аварийного завершения сначала убедитесь, что старый процесс отсутствует, затем удалите только stale lock; не удаляйте `state.json`. Истёкший quote и изменение согласованной суммы требуют разбора, а не удаления состояния и повторной покупки. Это учебный файловый журнал, не production-очередь: для реального биллинга используйте транзакционную БД, блокировки, защиту журналов и устойчивый scheduler. При остановке после отправки и до сохранения ответа HTTP-idempotency восстанавливает запрос лишь в пределах своего срока хранения; после длительного простоя сначала сверяйте external_id/UUID, не запускайте записи вслепую. ## Использовать клиент в своей задаче ```js import { createClient, createIntent } from './transport.mjs'; const api = createClient({ baseUrl: process.env.RESELLER_API_BASE, token: process.env.RESELLER_API_TOKEN, }); const intent = createIntent('PUT', `/domains/${domainUuid}/nameservers`, { nameservers: [{ hostname: 'ns1.example.net' }, { hostname: 'ns2.example.net' }], reason: 'Согласованная смена DNS-провайдера', }); // Сначала транзакционно сохранить intent в своей БД; при повторе загрузить его оттуда. await intentStore.insert(intent); const accepted = await api.request(intent); await intentStore.attachOperation(intent.key, accepted.body.data.uuid); const operation = await api.operation(accepted.body.data.uuid); if (operation.status === 'completed') { const domain = await api.request(createIntent('GET', `/domains/${domainUuid}`)); await domainStore.reconcile(domain.body.data); } else { await intentStore.deferForReconciliation(intent.key, operation); } ``` `intentStore` и `domainStore` обозначают ваш слой хранения, их нужно реализовать. Пример транспорта не координирует бюджет rate limit между несколькими процессами: общий ограничитель ключа добавляется в вашем scheduler. Длительный `Retry-After` возвращается как ошибка с задержкой для планировщика, а не сокращается до удобного значения. Полная схема подписанного webhook и пример проверки HMAC приведены в [разделе уведомлений](webhooks.md). Не используйте этот исходящий API-клиент как проверку входящей подписи. --- SOURCE: /guide/integration.md # Требования к интеграции Этот документ можно использовать как задание разработчику или ИИ вместе с [OpenAPI](/openapi.json), [полным текстом](/llms-full.txt) и примерами. Генерация клиента по схеме не реализует автоматически финансовую логику, согласие владельца, обработку неизвестного результата или защиту секретов. ## Входные данные задания До реализации зафиксируйте URL и релиз API, режим клиентов, окружение, валюты, разрешённые зоны, scopes, исходящие IP, webhook URL, правила вашего розничного биллинга и место хранения ключей. Отсутствующие значения запрашиваются у оператора; их нельзя угадывать по примерам. Источником HTTP-контракта является OpenAPI этого релиза. Руководства определяют порядок операций и ограничения, которые не выражаются JSON Schema. При противоречии остановите выпуск интеграции и уточните контракт, а не выбирайте случайный вариант. ## Загрузка документации в ИИ Начните с индекса [llms.txt](/llms.txt), затем загрузите руководства нужного сценария, [OpenAPI](/openapi.json) и Markdown-страницы задействованных методов по ссылкам индекса. Такой порядок даёт модели сначала процесс и ограничения, затем точные поля запросов и ответов. [llms-full.txt](/llms-full.txt) содержит полный материал, но может превышать контекст модели: используйте поиск и загрузку фрагментов по HTTP-методу и пути, добавляя общие правила авторизации, ошибок, идемпотентности и пагинации. Не передавайте модели настоящие bearer-ключи, auth code, секреты восстановления или webhook, ЭЦП и персональные данные клиентов. Используйте синтетические примеры. Документация не гарантирует, что любая модель автоматически создаст правильную интеграцию: сгенерированный код требует проверки разработчиком и прохождения приёмочных сценариев ниже. ## Локальная модель Храните соответствия собственных ID с `customer_uuid`, contact UUID, order UUID, `service_uuid`, domain UUID и operation UUID, разделённые по реселлеру и окружению. Для каждого намерения изменения сохраняйте до отправки: - собственный неизменяемый ID намерения и бизнес-основание; - HTTP-метод, путь с query, точные байты JSON и `Idempotency-Key`; - состояние отправки, число попыток, время следующей проверки; - принятые UUID, HTTP-статус и `X-Request-ID` без секретов; - подтверждённый результат либо отметку неопределённости. Защитите уникальным ограничением одновременное создание двух намерений для одной покупки. После сбоя процесса возобновляйте сохранённое намерение, а не конструируйте новую покупку из формы пользователя. Webhook inbox также должен иметь уникальный ключ события. ## Обязательное поведение 1. Проверять права пользователя собственного биллинга до выбора UUID и вызова API. 2. Отправлять ключ только на фиксированный доверенный API origin; запретить следование redirect с авторизацией. 3. Не смешивать test и live, не переносить между ними UUID и quotes. 4. Использовать суммы quote в целых minor units; показывать согласованную цену до платной операции. 5. Разделять создание заказа регистрации и последующие асинхронные операции управления. 6. Сохранять идемпотентный ключ и тело до первого сетевого вызова; не менять их при timeout/5xx. 7. Ограничивать повторы и частоту polling; учитывать `Retry-After`, общий бюджет ключа и jitter. 8. Не считать `201`/`202` завершением; сверять фактические даты и состояние после результата. 9. Для `uncertain` не создавать вторую команду и не возвращать деньги автоматически без финансовой сверки. 10. Проверять подпись webhook по исходным байтам, фиксировать событие надёжно до 2xx, терпеть повторы и перестановку событий. 11. Дополнять webhooks периодическим чтением: уведомление не является единственным источником состояния. 12. Не логировать Bearer, подписи, auth code, restore secret и персональные поля контактов. ## Приёмочные сценарии | Проверка | Ожидаемый результат | | --- | --- | | Неверный/отозванный ключ | Контролируемая ошибка; бесконечные повторы отсутствуют. | | Ключ другого реселлера или окружения | Нет доступа к чужим клиентам, услугам и операциям. | | Недостаточный scope и запрещённый IP | Отказ виден оператору интеграции, не выдаётся за отсутствие домена в реестре. | | Managed и aggregate | Верное создание/выбор клиента без смешения владения. | | Пагинация более одной страницы | Все UUID обработаны, курсор и фильтры сохранены, дубликаты безопасны. | | Истёкший quote до покупки | Новая цена согласована до нового заказа. | | Недостаточный баланс | Нет ложного успешного заказа и повторного розничного списания. | | Имя заняли после проверки | Отказ регистрации обработан отдельно от оплаты. | | HTTP timeout после принятия запроса | Повтор прежнего намерения не создаёт вторую услугу/операцию. | | Одинаковый ключ, иное тело | `409` обрабатывается как конфликт, ключ автоматически не заменяется. | | Перезапуск процесса между отправкой и сохранением ответа | Сохранённые ключ/тело восстанавливают тот же запрос. | | 429 | Учитывается пауза; все workers с этим ключом не создают повторный всплеск. | | Ответ HTML/502 от proxy | Сохраняется техническая ошибка без попытки трактовать её как Domain. | | EPP выполнил команду, ответ потерян | `uncertain` не превращается в повторную платную команду. | | Продление до 24 часов / сверх 10 лет | Запрет показан до покупки; серверный отказ также обработан. | | Успешное продление | Итоговый `expires_at` берётся из платформы, не локально прибавленным годом. | | Перенос pending/отказ/отмена | Сохраняются различимые состояния, резерв и услуга сверяются. | | Подмена webhook, повтор, неверный timestamp | Подмена отклоняется; разрешённый повтор не дублирует бизнес-эффект. | | События пришли не по порядку | Позднее старое событие не откатывает подтверждённое состояние; выполняется GET-сверка. | | Отказ автопродления | Создано уведомление ответственному, обещание продления покупателю не выдано. | | Неизвестный enum/код/дополнительное поле | Клиент не падает и не признаёт неизвестную операцию успешной. | Прогоняйте платные и изменяющие сценарии в sandbox с разрешёнными тестовыми данными. Для live сначала согласуйте ограниченную контрольную операцию с оператором и владельцем домена. Отчёт о прохождении должен содержать окружение, релиз и request/operation ID без секретов. ## Что не реализовывать по догадке Не добавляйте вымышленные endpoints оплаты, отмены любой операции, управления DNS-записями, DNSSEC, редактирования чужого клиента или мгновенного возврата через платёжный шлюз. Не используйте POST повторно новым ключом только потому, что GET ещё не показывает изменение. Не считайте одинаковый domain name достаточной связью между test/live. Для обращения в поддержку передавайте время UTC, endpoint, HTTP-статус, машинный код, request ID, order/domain/operation UUID и описание ожидаемого результата. Данные подписи, секреты и полный профиль владельца в диагностический пакет не включаются. --- SOURCE: /guide/quickstart.md # Быстрый старт ## Получить доступ Попросите оператора выдать URL API, тестовый ключ, режим клиентов (`managed` или `aggregate`), список разрешённых зон и sandbox-данные реестра. Для live дополнительно согласуйте валюту, тарифы, способ расчётов и пополнение оптового баланса. Исходящий IP вашего сервера должен входить в allowlist ключа, если она настроена. Ключ создаётся оператором и показывается целиком только при выдаче или ротации. Храните его в серверном secret storage. Не помещайте ключ в браузер, мобильное приложение, Git, URL или журнал запросов. ```bash export RESELLER_API_BASE='https://api.b.websoft.kz/api/reseller/v1' # Значение получить у оператора; строка ниже не является действующим ключом. export RESELLER_API_TOKEN='rsl_test_REPLACE_KEY.REPLACE_SECRET' curl --fail-with-body --silent --show-error \ "$RESELLER_API_BASE/context" \ -H "Authorization: Bearer $RESELLER_API_TOKEN" \ -H 'Accept: application/json' ``` В исходнике руководства URL задан шаблоном, который портал заменяет значением `DOCS_API_URL` при сборке/запуске. Оператор должен настроить этот адрес для нужного окружения; перед отправкой ключа проверьте доверенный origin опубликованного примера. Сверьте `data.credential.environment`, `scopes`, `rate_limit_per_minute` и `data.provider.customer_mode`. Не продолжайте тестовые записи, если вернулся `live`. `401` означает проблему аутентификации; `403` может означать запрещённый IP, состояние реселлера, план или недостаточные права; `404` также возможен, когда API отключён оператором. Подробности в [обработке ошибок](reliability.md). ## Необходимые права | Сценарий | Scopes | | --- | --- | | Каталог и проверка домена | `catalog.read`, `domains.read` | | Клиенты и контакты | `customers.read`, `customers.write`, `contacts.read`, `contacts.write` | | Регистрация | `quotes.create`, `orders.create`, `domains.register`, `orders.read`, `domains.read` | | Чтение услуг | `services.read` | | Продление | `quotes.create`, `domains.renew`, `domains.read` | | Перенос | `quotes.create`, `domains.transfer`, `domains.read` | | Восстановление | `quotes.create`, `domains.restore`, `domains.read` | | NS, контакты, блокировки, приватность, glue | `domains.manage`, `domains.read` | | Код переноса | `domains.auth-code`, `domains.read` для чтения результата | | Удаление | `domains.delete`, `domains.read` | | Подтверждение владельца | `domains.verify` | | Уведомления | `webhooks.read`, `webhooks.manage` | | Баланс и движения, только live | `balance.read`, `billing.read` | | Обращения, только live | `tickets.read`, `tickets.write` | | Заказ хостинга | `quotes.create`, `orders.create`, `hosting.order` | Для включения автопродления нужны одновременно `domains.manage` и `domains.renew`; для выключения достаточно `domains.manage`. Scopes не заменяют поддержку операции регистратором. ## Первый запрос с телом Даже проверка доступности использует `POST` и требует идемпотентный ключ. Пример ниже проверяет один домен, не регистрирует его. ```bash CHECK_KEY="$(node -e 'console.log(crypto.randomUUID())')" curl --fail-with-body --silent --show-error \ "$RESELLER_API_BASE/domains/check" \ -H "Authorization: Bearer $RESELLER_API_TOKEN" \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: $CHECK_KEY" \ --data-raw '{"domain":"example.kz"}' ``` Результат проверки не резервирует имя и может измениться до регистрации. Для новой проверки создаётся новый ключ; для повтора потерянного ответа прежней проверки сохраняются старый ключ и те же байты тела. Не генерируйте ключ внутри автоматической повторной попытки. ## Порядок подключения 1. Получить контекст и убедиться в тестовом окружении. 2. Прочитать каталог, выбрать доступную зону и период. 3. Создать клиента, если используется managed; в aggregate клиент назначен оператором. 4. Создать контакт и сохранить его UUID. 5. Проверить имя, получить quote, создать заказ. 6. Дождаться результата регистрации, затем проверить чтение, NS и продление в sandbox. 7. Подключить подписанные webhooks и резервную периодическую сверку. 8. Выполнить [приёмочные проверки](integration.md), затем получить отдельный live-ключ. Тестовое окружение не является офлайн-симулятором: оно может обращаться к тестовому серверу регистратора. Нужны его действительные тестовые контакты и разрешённые домены. Тестовые UUID и ключи не переносятся в live. Баланс, ledger и tickets в test запрещены. --- SOURCE: /guide/registration.md # Регистрация домена Этот сценарий предназначен для server-to-server интеграции с собственным биллингом и внешними расчётами. В каждом примере JSON замените UUID результатами предыдущих шагов. Для каждого нового POST нужен отдельный `Idempotency-Key`; при сетевом повторе того же шага ключ и тело сохраняются. Общие заголовки: `Authorization: Bearer `, `Accept: application/json`, `Content-Type: application/json`. Подробные ответы каждого шага представлены в справочнике методов. [Исполняемый пример](examples.md) сохраняет промежуточный прогресс. ## 1. Выбрать зону Вызовите `GET /catalog/domain-zones`. Выберите разрешённое предложение и проверьте `allowed_operations`, `allowed_periods` и ограничения зоны. В quote используется `resource_uuid` зоны, а не `uuid` предложения. Перед live-покупкой перечитывайте каталог и цену: вчерашний тариф не является основанием для списания. ## 2. Создать клиента в managed `POST /customers`: ```json { "external_id": "customer-1001", "type": "individual", "display_name": "Ivan Petrov", "email": "owner@example.net", "first_name": "Ivan", "last_name": "Petrov", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty" } ``` Сохраните `data.uuid` как `customer_uuid`. Перед повторным созданием после утраты локального состояния выполните `GET /customers?external_id=customer-1001`. Не создавайте новый external_id для того же клиента из-за таймаута. Для компании применяется `type: legal` с реквизитами юридического лица согласно схеме. Тип клиента и тип доменного контакта имеют разные названия: `individual`/`legal` у клиента, `person`/`organization` у контакта. В aggregate этот шаг пропускается; не отправляйте произвольный UUID покупателя. ## 3. Создать контакт `POST /contacts`: ```json { "customer_uuid": "11111111-1111-4111-8111-111111111111", "external_id": "contact-1001", "type": "person", "first_name": "Ivan", "last_name": "Petrov", "middle_name": "Ivanovich", "email": "owner@example.net", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "address_line": "Example street 1", "postal_code": "050000" } ``` В aggregate опустите `customer_uuid`. Сохраните `data.uuid`. Контакт из этого примера показывает структуру, но не гарантирует пригодность для каждой зоны: NIC.KZ может требовать ИИН/БИН и документы. Используйте реальные законно полученные данные владельца либо согласованные sandbox-данные. Не выдумывайте идентификатор с подходящей длиной: проверяются формат, контрольная сумма и требования зоны. Для организации укажите `type: organization` и `organization_name`; идентификатор передаётся парой `external_id_type` и `external_id_value`. Это идентификатор документа, а `external_id` выше является ключом объекта вашего биллинга. Требования `.edu.kz`, включая тип документа и образовательную лицензию, проверяются отдельно от создания контакта. Один контакт можно назначить всем четырём ролям, когда это соответствует реальным полномочиям. Контакты должны находиться в том же клиенте, реселлере и окружении. API не требует вручную создавать EPP contact ID. ## 4. Проверить доступность `POST /domains/check`: ```json {"domain":"example.kz"} ``` Обработайте занятое имя и невозможность проверки как разные результаты согласно схеме ответа. Доступность на момент проверки не является гарантией регистрации: между проверкой и командой имя может занять другой заявитель. ## 5. Зафиксировать цену `POST /quotes`: ```json { "customer_uuid": "11111111-1111-4111-8111-111111111111", "items": [ { "resource_type": "domain_zone", "resource_uuid": "22222222-2222-4222-8222-222222222222", "operation": "register", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "example.kz", "external_id": "domain-1001", "contact_uuids": { "owner": "33333333-3333-4333-8333-333333333333", "admin": "33333333-3333-4333-8333-333333333333", "tech": "33333333-3333-4333-8333-333333333333", "billing": "33333333-3333-4333-8333-333333333333" }, "nameservers": [ {"hostname":"ns1.example.net"}, {"hostname":"ns2.example.net"} ] } } ] } ``` Сохраните UUID quote, срок действия, валюту и суммы. В aggregate опустите `customer_uuid`. Проверьте сумму перед подтверждением покупки. Входящие параметры quote описывают будущую услугу; они не являются свободным хранилищем ваших данных. Если quote истёк до создания заказа, получите новую цену и повторно согласуйте её при необходимости. Если отправка заказа уже произошла и её исход неизвестен, не создавайте новый quote и заказ до сверки. ## 6. Создать заказ `POST /domains` (алиас доменной регистрации через создание заказа): ```json { "quote_uuid": "44444444-4444-4444-8444-444444444444", "customer_uuid": "11111111-1111-4111-8111-111111111111", "external_id": "order-1001" } ``` Ответ `201` содержит **Order**, а не Domain или Operation. Сохраните `data.uuid`, `data.items[].service_uuid`, `data.items[].service_external_id` и `data.invoice`, если он не `null`. В aggregate опустите `customer_uuid`. `order-1001` относится к заказу, `domain-1001` из quote относится к будущей услуге. При external checkout платформа выполняет предусмотренные оптовые расчёты и ставит регистрацию на исполнение. Если оператор назначил иной режим checkout, создание заказа само по себе может не запустить оказание услуги; не обходите оплату произвольными параметрами. ## 7. Дождаться результата Читайте `GET /orders/{orderUuid}` и `GET /domains?external_id=domain-1001`; используйте backoff и webhooks. Начальная регистрация не является задачей `/operations`: этот раздел предназначен для последующих операций управления. После появления Domain сохраните его `uuid` отдельно от `service_uuid`. Проверьте `registration_status`, `registrar_operation_status`, `registered_at`, `expires_at` и статусы реестра. Оплаченный заказ не доказывает отсутствие `serverHold` или успешную проверку лицензии. | Ситуация | Действие | | --- | --- | | Заказ принят, регистрация ещё выполняется | Оставить локальный заказ в обработке, продолжать сверку. | | Регистрация подтверждена | Сохранить реальные даты и контакты; показать результат покупателю. | | Подтверждён отказ, например имя занято | Сохранить причину, сверить финансовый результат и применить свою политику возврата. | | Таймаут HTTP | Повторить только исходный запрос с прежним ключом и байтами либо восстановить заказ по external_id. | | Неизвестен результат EPP | Не создавать новый заказ. Передать UUID заказа/домена и request ID поддержке. | | Домен зарегистрирован, но требуется ЭЦП | Выполнить отдельный поток подтверждения владельца. | Автоматическая сверка неизвестного результата не означает автоматическую повторную отправку регистрации. При неизвестном исходе средства могут оставаться в резерве до подтверждения. --- SOURCE: /guide/reliability.md # Надёжность интеграции Этот раздел описывает действующее поведение REST API, а не желаемый стандарт. Базовый путь всех примеров: `/api/reseller/v1`. Используйте выданный провайдером HTTPS origin. `api.example` в примерах является обозначением, не рабочим сервером. Значения по умолчанию приведены для текущей версии; фактические настройки вашего API-ключа и webhook имеют приоритет. ## Авторизация и контекст Передавайте `Authorization: Bearer $RESELLER_API_TOKEN`, `Accept: application/json`, а для JSON-тела `Content-Type: application/json`. Храните токен только на сервере интеграции. Не передавайте его браузеру, в query string, тикете, публичном репозитории или ИИ вместе с документацией. Формат ключа: `rsl_test_{publicKeyId}.{secret}` или `rsl_live_{publicKeyId}.{secret}`. `publicKeyId` содержит 24 латинские буквы/цифры, `secret` содержит 64 символа base64url. Ключ личного кабинета не заменяет reseller-ключ. Среду нельзя переключить заголовком или полем запроса. Для `test` и `live` нужны разные ключи, данные и настройки подключения. Начните с `GET /context`: он не требует отдельного scope и возвращает среду, доступные scopes и `data.credential.rate_limit_per_minute`. На остальные методы нужны scopes, указанные в справочнике; требование нескольких scopes означает логическое И. Наличие `domains.read` не даёт права покупать или продлевать домен. Учитываются также IP allowlist, активность ключа, срок его действия, режим API, тариф и статус реселлера. Ограниченный реселлер может выполнять безопасные HTTP-методы, но не POST, даже если POST логически используется для проверки. Ресурсы ограничены реселлером и средой. Чужой или недоступный ресурс обычно даёт 404, а не сведения о его владельце. Передавайте UUID ровно из ответов API: не все методы одинаково обрабатывают произвольный не-UUID, возможен 500 вместо ожидаемого 422. ### Контрпример: поддержка в test Не считайте, что test-ключ автоматически предоставляет песочницу для всех методов. Все четыре обработчика поддержки доступны только с live-ключом, независимо от наличия `tickets.read`/`tickets.write`: | Метод | Путь | Результат с корректным test-ключом и нужным scope | | --- | --- | --- | | GET | `/tickets` | 403 | | GET | `/tickets/{ticketUuid}` | 403 | | POST | `/tickets` | 403 | | POST | `/tickets/{ticketUuid}/messages` | 403 | ```json {"error":{"code":"forbidden","message":"Support is available only to live credentials.","details":[],"request_id":"11111111-1111-4111-8111-111111111111"}} ``` Для записей это ограничение проверяется также перед возвратом старого HTTP replay. `/balance` и `/ledger` тоже не являются тестовыми финансовыми ресурсами: test-ключ получает 403. Для проверки поддержки используйте согласованный с провайдером live-сценарий, не переключайте финансовые тесты на live автоматически. ## Лимиты запросов | Параметр | Поведение | | --- | --- | | Базовый лимит нового ключа | 120 запросов за 60 секунд, может быть изменён провайдером | | Настраиваемый диапазон ключа | 1..10000 запросов за минутное окно | | Область счётчика | Одна запись API-ключа, все её методы и источники IP вместе | | Окно | Фиксированное окно 60 секунд с первого учитываемого запроса; не календарная минута, не скользящий час и не token bucket | | Сброс | По окончании окна; последующие запросы его не продлевают | | Учёт | Запросы после успешной проверки ключа/реселлера, до проверки Idempotency-Key и scope; ошибки валидации, scope и HTTP replay тоже расходуют квоту | | Отказы | Ранний отказ авторизации/реселлера не расходует этот счётчик. Собственный 429 не добавляет hit и не продлевает паузу | | Ротация API-ключа | Меняет секрет существующего ключа, не обещает новый счётчик | При конкурентных запросах проверка квоты и увеличение счётчика раздельны. Не используйте кратковременное превышение как разрешённую ёмкость; ограничивайте параллелизм и распределяйте запросы равномерно. Это лимит REST API, не квота реестра и не гарантия скорости выполнения доменной операции. После прохождения ограничителя возвращаются: ```http X-RateLimit-Limit: 120 X-RateLimit-Remaining: 119 X-Request-ID: 11111111-1111-4111-8111-111111111111 ``` На собственной ветке исчерпания квоты ответ имеет вид: ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 42 X-Request-ID: 11111111-1111-4111-8111-111111111111 ``` ```json {"error":{"code":"rate_limit_exceeded","message":"API rate limit exceeded.","details":{"retry_after":42},"request_id":"11111111-1111-4111-8111-111111111111"}} ``` В этой ветке **нет** `X-RateLimit-Limit` и `X-RateLimit-Remaining`. На успешном ответе **нет** `Retry-After`. API самостоятельно не формирует `X-RateLimit-Reset`, `RateLimit`, `RateLimit-Policy` и `RateLimit-Reset`. Промежуточный proxy может добавить собственные заголовки или отдельный 429. Не требуйте отсутствующие заголовки в SDK. Сохранение чужих rate-limit заголовков при обработке ошибки не означает, что API их всегда выдаёт. На 429 приостановите всю очередь данного ключа на `Retry-After` секунд плюс небольшой случайный запас. Если заголовка нет, используйте ограниченный exponential backoff с jitter. При `Retry-After: 0` не запускайте busy loop. Для общего HTTP-клиента поддержите также HTTP-date от proxy; собственный ограничитель API возвращает целые секунды. `X-RateLimit-Remaining` является снимком, а не резервированием места для следующего запроса. ## Идемпотентность `Idempotency-Key` обязателен для **всех POST, PUT, PATCH и DELETE**, включая `/domains/check`, `/quotes`, `/domains/{domainUuid}/sync`, webhook test и ротацию секрета. GET/HEAD его не требуют. Значение обрезается по краям и должно содержать 1..160 символов. Рекомендуется UUID либо ваш уникальный идентификатор логической команды без персональных данных. Сохраните до отправки: ключ, API-ключ/среду, HTTP-метод, путь, query string и исходные байты тела. SHA-256 отпечаток учитывает метод, путь с query string в его HTTP-нормализованном представлении и **точные байты тела**, а не семантически равный JSON. Не меняйте порядок полей, пробелы, число `1` на `1.0`, пустое тело на `{}` и путь `/orders` на `/domains` при повторе. Заголовки языка и X-Request-ID в отпечаток не входят. | Ситуация | Результат | | --- | --- | | Первый запрос с новым ключом | Обычная обработка | | Тот же ключ и отпечаток, HTTP-ответ сохранён | Тот же HTTP status/JSON, `Idempotency-Replayed: true` | | Тот же ключ, другой отпечаток | 409 `conflict` | | Тот же ключ, первая попытка ещё processing | 409 `conflict` | | Нет/невалидный ключ | 422 `validation_failed`, `details: []` | | Исключение или ответ 5xx | Сохранённый HTTP replay не гарантируется | Обычный TTL HTTP replay: **24 часа с момента первого резервирования**, настраивается провайдером и не продлевается повторами. Истёкшая запись может быть удалена при следующем обращении с этим ключом. Зависшая processing-запись не имеет отдельного короткого автоматического срока разблокировки: не обходите 409 новым ключом после таймаута. Уточните состояние у поддержки. Доменная асинхронная операция имеет дополнительную устойчивую привязку ключа к задаче, которая не равна HTTP TTL. Однако у других методов такой защиты может не быть. **Никогда не используйте старый ключ для нового бизнес-действия, в том числе спустя 24 часа.** После истечения TTL сначала сверяйте ресурсы по UUID/external_id и операции, а не создавайте заказ заново. Проверка действительности Bearer и общего доступа выполняется перед replay, но свежая проверка scope конкретного метода не гарантируется для уже сохранённого ответа. Отзыв ключа закрывает доступ; простое снятие scope не следует использовать как гарантию немедленного запрета чтения ранее сохранённых ответов. Это важно для чувствительных ответов, например секрета webhook. HTTP replay повторяет статус и JSON, но не является побайтовым воспроизведением всех заголовков. `Idempotency-Replayed` отсутствует на обычном ответе, а не равен false. `X-Request-ID` может быть новым при старом `error.request_id` в сохранённом теле. При ротации/создании webhook replay может снова показать секрет: исключите эти ответы из логирования и кешей общего пользования. Срок TTL не является обещанием физического удаления секрета из хранилища в ту же секунду. ## Ошибки и безопасные решения API использует собственный JSON-конверт, **не** `application/problem+json` и **не** RFC 9457: ```json {"error":{"code":"validation_failed","message":"The name field is required.","details":{"name":["The name field is required."]},"request_id":"11111111-1111-4111-8111-111111111111"}} ``` Пустые `details` выглядят как `[]`; объект ошибок полей содержит массивы строк. На 429 объект содержит число `retry_after`. `details: null` для этого конверта не ожидается. `request_id` допускает null, если контекст не назначен. Само поле `error` в успешном JSON ресурса операции имеет другую семантику и может быть null; не смешивайте его с HTTP ErrorEnvelope. | HTTP / code | Решение интегратора | | --- | --- | | 400 / `request_failed`, если конверт применён | Исправить запрос/JSON; не повторять неизменённым бесконечно | | 401 / `unauthenticated` | Исправить токен, срок, отзыв; не создавать новый заказ в ответ на ошибку авторизации | | 403 / `forbidden` | Проверить scopes, IP, режим/тариф/статус реселлера и live-only ограничения | | 404 / `resource_not_found` | Проверить путь, UUID, среду и владельца; также возможно отключение API | | 405 / `request_failed`, только если конверт применён | Выбрать документированный метод; маршрутизатор часто возвращает только `message` | | 409 / `conflict` | Отличить конфликт ключа/обработку от состояния ресурса по локальному журналу и GET; не автоматический новый ключ | | 422 / `validation_failed` | Показать ошибки полей, исправить входные данные; для изменённого запроса использовать новый ключ после сверки | | 429 / `rate_limit_exceeded` | Общая пауза ключа по Retry-After, затем повтор той же команды | | 500 и прочие 5xx / `internal_error`, если конверт применён | Возможен уже выполненный побочный эффект; сверка и ограниченный повтор с прежним ключом | | Timeout, разрыв соединения, ответ не JSON | **Результат неизвестен**, не эквивалент failed; сохранить попытку и сверять | Неподдерживаемый метод, неизвестный маршрут, неверный запрос до маршрутизации, сетевой proxy или WAF могут вернуть другой JSON, HTML или пустое тело без X-Request-ID. SDK должен сначала проверить HTTP status и Content-Type и безопасно сохранить ограниченный диагностический фрагмент. Не считать любую ошибку декодирования JSON признаком отказа бизнес-операции. Язык выбирается из поддерживаемых `ru`, `en`, `kk`: в первую очередь `X-Language`, затем `X-Locale`, затем доступный контекст/`Accept-Language` и язык по умолчанию. Региональный суффикс явного заголовка, например `ru-RU`, нормализуется. Не все сообщения переведены: английская строка возможна при русском запросе. `code`, имена полей и структура не меняются от языка. Ветвление по `message.includes(...)` запрещено в интеграции: несколько разных причин сейчас используют один `conflict` или `forbidden`. ### Пример неопределённой операции HTTP 202 подтверждает приём, не результат реестра. После ответа сохраните UUID задачи и опрашивайте `GET /operations/{operationUuid}`; webhook используйте как ускоряющий сигнал, не единственное доказательство. ```json {"data":{"uuid":"66666666-6666-4666-8666-666666666666","domain_uuid":"55555555-5555-4555-8555-555555555555","action":"renew","status":"uncertain","result":[],"error":{"code":"registry_result_unknown","message":"Registry result is not confirmed. Do not repeat this operation; reconciliation is scheduled."},"created_at":"2026-10-05T09:00:00+00:00","completed_at":null,"next_check_at":"2026-10-05T09:05:00+00:00"}} ``` Текущий обработчик REST-операций выставляет `pending`, `processing`, `uncertain`, `completed` и `failed`. Первые три не являются отказом. `awaiting_registry` учтён защитной проверкой занятости домена, но в текущем коде не найден процесс, который его выставляет; это не этап публичного жизненного цикла REST-операции. Если получите незнакомое состояние, сохраните его и сверяйте через GET/поддержку, не приравнивайте к failed. Не освобождайте собственный резерв клиента и не создавайте вторую команду только из-за таймаута. Завершение `completed` и подтверждённый `failed` обрабатывайте по справочнику операции. Не все неопределённые результаты можно автоматически сверить: запросите ручную проверку, сохранив UUID задачи, домена и запроса. Регистрация создаётся через заказ; после неизвестного ответа ищите заказ/услугу по external_id и её состояние, не ожидайте, что у регистрации обязательно будет тот же публичный формат задачи управления доменом. ## Алгоритм клиента 1. До сетевого вызова сохраните неизменяемую команду и её Idempotency-Key в своей БД. 2. Пропустите её через общую очередь лимита API-ключа. X-Request-ID задавайте отдельно на попытку. 3. При 2xx сохраните ответ атомарно с локальной командой; 204 не декодируйте как JSON. При 202 запланируйте проверку UUID операции. 4. При 429 выдержите паузу для всего ключа. При сетевой ошибке/5xx пометьте команду как unknown, сверяйте состояние и при допустимом повторе не меняйте ключ/байты. 5. При 401/403/404/405/422 остановите автоматические повторы до исправления причины. При 409 сверяйте исходную команду и текущее состояние, не обходите блокировку. 6. При uncertain продолжайте редкий polling, учитывая next_check_at и общую квоту. При длительном ожидании создайте обращение без секретов и повторной покупки. 7. Не делайте вывод о регистрации, оплате или возврате средств только по отсутствию webhook. ## Пагинация и журнал интегратора Списки с cursor используют `per_page=25` по умолчанию, диапазон 1..100. Передавайте `next_cursor` без изменений и с URL-кодированием. В webhook-списках ответ содержит только `data` и `meta.next_cursor/per_page`: поля `total`, `page` и `last_page` не обещаны. Некоторые другие ресурсы дополняют пагинацию links; проверяйте контракт конкретного метода. Отсутствующий курсор на первой странице и `next_cursor: null` на последней не одно и то же. Храните у себя среду, метод/путь без секретов, время, HTTP status, код ошибки, X-Request-ID, Idempotency-Key, UUID заказа/домена/операции и результат сверки. Не пишите Authorization, signing_secret, auth-code, ЭЦП и персональные документы в общие логи. Ошибки авторизации до определения ключа могут не попасть в журнал запросов провайдера. Значение хранения журнала в конфигурации по умолчанию 90 дней не является подтверждённым SLA доступности/удаления логов и не заменяет ваш журнал. ## Основания документации Справочник описывает поля, ограничения и ответы, а руководство отдельно объясняет порядок интеграции: такое разделение соответствует [Diataxis](https://diataxis.fr/). Полнота описания публичных элементов и ясность формулировок взяты из [Google AIP-192](https://google.aip.dev/192). Машинный контракт использует [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.1.html). Семантика HTTP и Retry-After опирается на [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), статус 429 на [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585.html#section-4). Это принципы оформления, а не заявление о реализации всех Google AIP или Problem Details. --- SOURCE: /guide/webhooks.md # Приём событий Webhook сообщает об изменении, но не заменяет GET ресурса и проверку окончательного результата. Доставка асинхронная, с ограниченным числом повторов; возможны дубликаты и нарушение порядка. Автоматическое завершение покупки только по отсутствию ошибки HTTP или наличию `domain.created` неверно: запись домена может быть создана до окончания регистрации. ## Создание и проверка подписки Понадобятся публичный HTTPS endpoint вашего биллинга, API-ключ со scope `webhooks.manage` и защищённое хранилище секрета. Для чтения подписок/доставок нужен `webhooks.read`. Все изменяющие запросы требуют Idempotency-Key. `$API_BASE` ниже включает `/api/reseller/v1`, `$RESELLER_API_TOKEN` берётся из хранилища секретов, а не из этого руководства. ```sh curl --fail-with-body "$API_BASE/webhooks" \ -H "Authorization: Bearer $RESELLER_API_TOKEN" \ -H 'Accept: application/json' \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: webhook-create-20261005-0001' \ --data-binary '{"name":"Domain events","url":"https://reseller.example/webhooks/billing","event_subscriptions":["domain.operation.updated","domain.registration.updated","domain.verification.updated"],"timeout_seconds":10,"max_attempts":8}' ``` `reseller.example` является нерабочим обозначением: замените его вашим endpoint с публичным DNS. HTTP 201: ```json {"data":{"uuid":"22222222-2222-4222-8222-222222222222","environment":"test","name":"Domain events","url":"https://reseller.example/webhooks/billing","event_subscriptions":["domain.operation.updated","domain.registration.updated","domain.verification.updated"],"timeout_seconds":10,"max_attempts":8,"status":"active","rotated_at":null,"disabled_at":null,"created_at":"2026-10-05T09:00:00+00:00"},"signing_secret":"whsec_EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0","secret_notice":"This signing secret is shown once and cannot be recovered."} ``` Секрет здесь демонстрационный. Храните настоящий секрет отдельно от токена REST API. GET подписки не возвращает секрет, но повтор создания/ротации с тем же Idempotency-Key в пределах срока HTTP replay может повторить первоначальный ответ с секретом. Не записывайте тело такого ответа в общие логи, аналитику браузера или чат. Подписка относится к реселлеру и среде, не одному API-ключу. Другой ключ того же реселлера и среды с нужным scope может управлять ею. Создание проверяет URL, но ещё не подтверждает доступность HTTPS-приёмника. Выполните test: ```sh curl --fail-with-body -X POST "$API_BASE/webhooks/22222222-2222-4222-8222-222222222222/test" \ -H "Authorization: Bearer $RESELLER_API_TOKEN" \ -H 'Accept: application/json' \ -H 'Idempotency-Key: webhook-test-20261005-0001' ``` HTTP 202: ```json {"data":{"uuid":"33333333-3333-4333-8333-333333333333","event_uuid":"44444444-4444-4444-8444-444444444444","event_type":"webhook.test","status":"pending","attempt_number":0,"http_status":null,"error_code":null,"next_attempt_at":"2026-10-05T09:01:00+00:00","delivered_at":null,"failed_at":null}} ``` `webhook.test` отправляется выбранной подписке даже без этого имени в event_subscriptions. Подписка должна быть active, иначе 409. Проверяйте `GET /webhooks/{webhookUuid}/deliveries` со scope `webhooks.read`: 202 означает очередь, а не успешную доставку. ## Транспорт и ограничения URL - Только HTTPS с проверкой сертификата, цепочки доверия и hostname/SNI. Нужен публично доверенный сертификат; self-signed без публичного доверия не поддерживается. Настройки custom CA и mutual TLS через этот API нет. - Допустим публичный hostname или глобальный IP с соответствующим сертификатом. Для IDN используйте ASCII punycode. Допустим явный корректный HTTPS port, не только 443. - Все разрешённые DNS A/AAAA проверяются. Если хотя бы один адрес частный/локальный/зарезервированный, multicast или запрещённый NAT64, URL отклоняется. DNS проверяется при создании, при изменении/включении и перед отправкой. - Соединение закрепляется за проверенным первым адресом. В одной попытке не обещается обход всех адресов DNS при сбое. TLS проверяет исходное имя, не подменённый IP. - Запрещены userinfo (`user:password@host`), URL fragment, localhost, пробельные/управляющие символы и обратные слеши. Query string разрешён, но секреты в нём передавать не следует. - Redirect не выполняется: 301/302/307/308 будут неудачной доставкой. Настройте точный конечный URL. Прокси-переменные отправителя не используются; произвольные дополнительные заголовки авторизации/Basic auth через API не настраиваются. - Протокол не обещает фиксированный исходящий IP. Не стройте allowlist по адресу, увиденному один раз; подпись обязательна независимо от сетевого фильтра. Смена DNS на запрещённый адрес после сохранения подписки приведёт к `transport_error` без обхода сетевой проверки, а не к отправке в частную сеть. PATCH `{"status":"disabled"}` с неизменным URL позволяет отключить подписку даже при неработающем DNS. ## Конверт и заголовки Отправитель выполняет POST с `Content-Type: application/json`, `Accept: application/json` и следующими прикладными заголовками: | Заголовок | Значение | | --- | --- | | `User-Agent` | `Billing-ITGroup-Webhook/1.0` | | `X-Webhook-ID` | UUID подписки | | `X-Webhook-Timestamp` | Unix time в секундах для текущей попытки | | `X-Webhook-Signature` | `v1=` + 64 lowercase hex символа HMAC-SHA256 | | `X-Request-ID` | UUID корреляции доставки; сохраняется при автоматическом повторе и redeliver | Отдельного `X-Webhook-Event`, номера попытки или delivery UUID в заголовках нет. REST Bearer-ключ отправитель вам не присылает. Тело события: ```json {"id":"44444444-4444-4444-8444-444444444444","type":"webhook.test","created_at":"2026-10-05T09:01:00+00:00","environment":"test","provider":"55555555-5555-4555-8555-555555555555","data":{"webhook_uuid":"22222222-2222-4222-8222-222222222222"}} ``` `id` идентифицирует событие, а не попытку. `created_at` относится к событию; при повторе меняется timestamp подписи, но не эта дата. `provider` допускает null при недоступной связи; приёмник для конкретного реселлера должен проверять ожидаемый UUID и среду. `event_version`, `request_id`, `correlation_id`, `delivery_uuid` и последовательного номера в теле нет. Не принимайте внутренние технические поля за часть публичного протокола. ## Проверка подписи Вычисление: ```text signature = "v1=" + hex_lowercase(HMAC_SHA256(signing_secret, timestamp + "." + raw_body)) ``` Используйте исходные байты HTTP-тела до JSON decode/re-encode. Подписываются timestamp и тело; X-Webhook-ID и X-Request-ID сами по себе не включены в HMAC. Сопоставляйте endpoint/секрет с ожидаемой подпиской, а `provider` и `environment` проверяйте в подписанном теле. Сравнивайте подписи constant-time функцией. Проверяйте также свежесть timestamp. Рекомендуемое окно приёмника: не более 300 секунд в прошлое/будущее с синхронизированными часами. Это **рекомендация для вашего приёмника**, не встроенный лимит REST API. Не проверяйте свежесть по created_at: старая дата события законна при повторе доставки. Подпись без контроля времени и дедупликации не защищает от replay. ### Воспроизводимый тест подписи Для точного однострочного JSON выше **без завершающего перевода строки**, timestamp `1791190860` и демонстрационного секрета `whsec_EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0` ожидается: ```text v1=55b46b56493f21432a77ad0e59e88eb824000c3528f7af5b8ea6459e903c96a8 ``` Это тест криптографического расчёта. При проверке в другой день реальный приёмник должен отвергнуть этот timestamp как устаревший. Для end-to-end используйте `/test`, не отключайте свежесть на рабочем endpoint. ### Пример приёмника PHP и PostgreSQL Пример принимает событие в устойчивую очередь вашего биллинга и отвечает 204 только после commit. Он не выполняет покупку/продление внутри webhook. Подготовьте таблицу в **своей** БД: ```sql CREATE TABLE billing_webhook_inbox ( webhook_uuid uuid NOT NULL, event_uuid uuid NOT NULL, provider_uuid uuid NOT NULL, environment text NOT NULL CHECK (environment IN ('test', 'live')), event_type text NOT NULL, raw_body text NOT NULL, received_at timestamptz NOT NULL DEFAULT now(), processed_at timestamptz, PRIMARY KEY (webhook_uuid, event_uuid) ); ``` Переменные окружения примера: `WEBHOOK_UUID`, `WEBHOOK_PROVIDER_UUID`, `WEBHOOK_ENVIRONMENT`, `WEBHOOK_SIGNING_SECRET`, необязательный `WEBHOOK_PREVIOUS_SECRET` только на переходный период, `INBOX_PDO_DSN`, `INBOX_DB_USER`, `INBOX_DB_PASSWORD`. Это параметры вашего приёмника, не требования к настройке сервера провайдера. ```php 300 || !hash_equals($webhook, $webhookHeader)) { reply(401); } $secrets = array_filter([$secret, (string) getenv('WEBHOOK_PREVIOUS_SECRET')]); $valid = false; foreach ($secrets as $candidate) { $expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $raw, $candidate); $matches = hash_equals($expected, $signature); $valid = $matches || $valid; } if (!$valid) { reply(401); } try { $event = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException) { reply(400); } $uuidPattern = '/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i'; if (!is_array($event) || !is_string($event['id'] ?? null) || !preg_match($uuidPattern, $event['id']) || !is_string($event['type'] ?? null) || !is_string($event['created_at'] ?? null) || !is_array($event['data'] ?? null)) { reply(400); } if (($event['provider'] ?? null) !== $provider || ($event['environment'] ?? null) !== $environment) { reply(403); } try { $db = new PDO( (string) getenv('INBOX_PDO_DSN'), (string) getenv('INBOX_DB_USER'), (string) getenv('INBOX_DB_PASSWORD'), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); $db->beginTransaction(); $insert = $db->prepare( 'INSERT INTO billing_webhook_inbox ' . '(webhook_uuid,event_uuid,provider_uuid,environment,event_type,raw_body) ' . 'VALUES (:webhook,:event,:provider,:environment,:type,:body) ' . 'ON CONFLICT (webhook_uuid,event_uuid) DO NOTHING' ); $insert->execute([ 'webhook' => $webhook, 'event' => $event['id'], 'provider' => $provider, 'environment' => $environment, 'type' => $event['type'], 'body' => $raw, ]); $db->commit(); } catch (Throwable) { if (isset($db) && $db->inTransaction()) { $db->rollBack(); } header('Retry-After: 60'); reply(503); } reply(204); ``` Отдельный обработчик вашей очереди берёт записи без processed_at, сверяет ресурс через REST и атомарно сохраняет бизнес-результат и отметку обработки. Уникальный event id предотвращает повторную вставку при сетевом таймауте после commit; сами финансовые/доменные действия также должны иметь собственный ключ идемпотентности. Если один endpoint обслуживает несколько подписок, выбирайте секрет только из заранее заданной таблицы подписок, а не из произвольного URL/поля входного запроса. Для неизвестного типа сохраните событие, отправьте 2xx после надёжной записи и поднимите внутреннее уведомление; не создавайте заказ по незнакомому payload. Отклонённый 401/400 не остановит автоматические повторы отправителя: исправляйте endpoint, а не полагайтесь на такой ответ как unsubscribe. ## Доставка, повторы и диагностика | Параметр | Значение | | --- | --- | | Успех | Любой HTTP 200..299, тело ответа не проверяется | | Неуспех HTTP | Любой не-2xx, включая 3xx, 400, 401, 404, 409 и 410 | | Общий timeout попытки | 10 секунд по умолчанию, настраивается 1..30 | | Connect timeout | min(5 секунд, общий timeout) | | Максимум попыток | 8 по умолчанию, настраивается 1..20, первая включена | | Базовая задержка после попытки n | min(21600, 60 × 2^(n-1)) секунд; cap 6 часов | | Поддержка Retry-After | Только ответы получателя 429 и 503; integer seconds или HTTP-date | | Максимально учитываемый Retry-After | 86400 секунд (24 часа) | | Итоговая пауза | max(базовая задержка, корректный Retry-After), не меньше базовой | | Проверка готовых повторов | Примерно раз в минуту плюс задержка очереди; точное время не гарантируется | При стандартных 8 попытках и отсутствии Retry-After задержки между попытками составляют 60, 120, 240, 480, 960, 1920, 3840 секунд. Суммарно 127 минут до восьмой попытки без учёта времени HTTP и очереди. После восьмой неуспешной попытки запись становится failed, следующая автоматически не планируется. Ограничение 6 часов относится к базовому backoff, не к итоговой паузе: `Retry-After` может увеличить её до 24 часов. Некорректное значение игнорируется, прошедшая дата даёт 0, стандартный backoff всё равно действует. На иных статусах Retry-After игнорируется. Серверного jitter в этой формуле нет. GET deliveries возвращает состояние последней попытки, не отдельную запись на каждую попытку: ```json {"data":[{"uuid":"33333333-3333-4333-8333-333333333333","event_uuid":"44444444-4444-4444-8444-444444444444","event_type":"webhook.test","status":"retry_scheduled","attempt_number":1,"http_status":503,"error_code":"http_error","next_attempt_at":"2026-10-05T09:02:00+00:00","delivered_at":null,"failed_at":null}],"meta":{"next_cursor":null,"per_page":25}} ``` | status / error_code | Значение и действие | | --- | --- | | pending / null | Ещё не выполнена попытка; не запускайте redeliver параллельно | | retry_scheduled / http_error | Получен не-2xx; исправьте endpoint, смотрите http_status и next_attempt_at | | retry_scheduled / transport_error | DNS, TLS, connect, timeout или запрет URL; HTTP status обычно null. Временной таймаут **не доказывает**, что получатель не сохранил событие | | delivered / null | Получен 2xx, delivered_at заполнено | | failed / http_error или transport_error | Лимит попыток исчерпан, next_attempt_at null; исправьте причину, затем redeliver | | cancelled / webhook_disabled | Получатель отключён, попытка не отправлена; включение само по себе не оживляет cancelled | | cancelled / delivery_scope_mismatch | Контекст доставки не совпадает с подпиской/событием; обратитесь к провайдеру, не обходите изоляцию | `response_excerpt`, HTTP-тело ответа приёмника, текст исключения, request_id и payload_hash в GET deliveries **не возвращаются**. Диагностику точной причины TLS/DNS/500 ведите по своему access/error log и запросу в поддержку провайдера с UUID доставки/события. В тело ошибки HTTP REST не подставляется error_code доставки. ## Ручной повтор и ротация `POST /webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver` разрешён только для delivered/failed/cancelled и active-подписки. Возвращает 202 с той же delivery UUID, сброшенным attempt_number=0, null в http_status/error_code/delivered_at/failed_at и новым next_attempt_at. Event id и X-Request-ID доставки сохраняются. Используются текущие URL и секрет. Нельзя использовать redeliver как способ повторно выполнить уже обработанное бизнес-действие: приёмник должен ответить 2xx на дубликат без побочного эффекта. Для повторной проверки работоспособности удобнее `/test`, создающий новое событие. PATCH может изменить timeout, max_attempts, URL и список подписок для дальнейших попыток. Он не отменяет уже начатый HTTP-вызов. Удаление webhook возвращает 204 без тела и удаляет связанные записи доставок. Не удаляйте подписку, если вам ещё нужны их UUID и история состояния. `POST /webhooks/{webhookUuid}/rotate-secret` немедленно заменяет секрет отправителя и возвращает новый. На стороне отправителя **нет grace period и одновременной подписи двумя ключами**. Отложенная попытка использует новый секрет, уже отправленная может прийти со старым. Практический порядок: 1. Подготовьте у приёмника поддержку двух секретов на короткий переходный период; старый пока основной. 2. Выполните rotate-secret один раз с сохранённым Idempotency-Key, получите новый секрет и установите его в приёмнике. Между ответом API и обновлением приёмника возможны неудачные попытки; они будут повторены. 3. Проверьте `/test` и реальные доставки. Уберите старый секрет после вашего окна допустимого запаздывания и свежести подписи. 4. При неизвестном результате rotate-secret сначала повторите **тот же** запрос с тем же ключом для HTTP replay, не запускайте следующую ротацию новым ключом вслепую. При критичной недопустимости такого окна согласуйте временное отключение/сверку: disabled не является гарантированным буфером событий. События, опубликованные во время отключения или до создания подписки, не обещаны к последующей доставке, а отменённые доставки требуют redeliver. Новая подписка не получает автоматически всю прошлую историю. ## События и payload Ниже перечислены все принимаемые имена. Подписки используют точное совпадение; wildcard `*` не поддерживается. Даже для действующего события не обещается уведомление на каждое изменение поля или каждое действие через все интерфейсы: периодическая REST-сверка обязательна. | Имя | Реальное назначение / data | | --- | --- | | customer.created | customer_uuid, external_id | | customer.updated | Либо customer_uuid/external_id, либо customer_uuid/contact_uuid/action для contact_created, contact_updated, contact_deleted | | domain.created | Снимок локального домена, не подтверждение регистрации | | domain.registration.updated | Изменение registration_status и снимок домена | | domain.operation.updated | operation_uuid, domain_uuid, action, status; результат читайте GET operation | | domain.updated | Снимок домена при отслеживаемом изменении, включая локальное удаление | | domain.verification.submitted | domain_uuid, external_id, verification_uuid, status; принятие подписи, не окончательная проверка | | domain.verification.updated | Снимок домена при изменении проверки владельца или образовательной лицензии | | domain.auto_renew.failed | domain_uuid, code=auto_renew_unavailable, обобщённое message; не более одного события на домен за календарный час | | domain.transfer.updated | Снимок домена с transfer.status и transfer.registry_transfer_status | | order.created | order_uuid, external_id, status; deleted может присутствовать в варианте lifecycle | | order.updated | order_uuid, external_id, status, deleted | | service.updated | service_uuid, external_id, status, deleted, auto_renew, expires_at | | ticket.created | ticket_uuid, external_id, status при создании через reseller API | | ticket.updated | ticket_uuid, message_uuid, status при ответе через reseller API; не обещает все ответы оператора через другие интерфейсы | | webhook.test | webhook_uuid | **Зарезервированные имена без действующих отправителей в текущей версии:** `domain.nameservers.updated`, `domain.contacts.updated`, `domain.renewed`, `domain.transfer.requested`, `hosting.created`, `hosting.updated`, `invoice.created`, `invoice.updated`, `payment.completed`. API принимает эти имена при создании подписки, но интеграция не должна ждать их для завершения заказа. Используйте `domain.operation.updated`, `domain.updated`, `order.updated`, `service.updated` и GET соответствующего ресурса. Для этих зарезервированных имён конкретная форма data пока не обещана. Пример фактического события операции: ```json {"id":"77777777-7777-4777-8777-777777777777","type":"domain.operation.updated","created_at":"2026-10-05T09:05:00+00:00","environment":"test","provider":"55555555-5555-4555-8555-555555555555","data":{"operation_uuid":"66666666-6666-4666-8666-666666666666","domain_uuid":"88888888-8888-4888-8888-888888888888","action":"renew","status":"completed"}} ``` Пример другой формы customer.updated: ```json {"id":"99999999-9999-4999-8999-999999999999","type":"customer.updated","created_at":"2026-10-05T09:03:00+00:00","environment":"test","provider":"55555555-5555-4555-8555-555555555555","data":{"customer_uuid":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa","contact_uuid":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb","action":"contact_updated"}} ``` Полные типы всех действующих payload описаны в машинном контракте по ссылке `#/components/schemas/WebhookEventEnvelope`; в `examples` есть полные конверты для всех 16 действующих имён. Исходящие заголовки описаны в `#/components/headers/WebhookId`, `#/components/headers/WebhookTimestamp`, `#/components/headers/WebhookSignature` и `#/components/headers/WebhookRequestId`. Эти заголовки относятся к входящему POST вашего приёмника, не к ответу REST API. При генерации приёмника не применяйте к нему Bearer security REST API: проверяется HMAC, а успешный ответ получателя может быть любым 2xx. Схема учитывает nullable поля и разные варианты customer.updated/order.created. В события не включаются auth-code домена, ЭЦП, пароль хостинга, тело обращения или signing_secret. Поля могут расширяться: игнорируйте неизвестные поля, но валидируйте обязательные для вашей обработки. ## Дедупликация, порядок и сверка Не обещаются exactly-once, глобальный порядок, последовательность событий одного домена и гарантированная доставка после исчерпания попыток. Таймаут может произойти **после** вашего commit: повтор с тем же event id нормален. Уникальность по событию внутри провайдера не означает, что один бизнес-переход всегда представлен ровно одним событием: могут прийти и domain.updated, и domain.registration.updated. Для одной подписки ключ inbox: `(webhook UUID, event id)`. Для общего бизнес-обработчика нескольких подписок дополнительно дедуплицируйте по `(environment, provider, event id)` и собственному ID операции/финансового действия. Не используйте X-Request-ID как ID бизнес-события и не сравнивайте UUID по порядку. Храните событие до 2xx. Если сохранение не удалось, отдайте 503 с Retry-After. Если сохранение удалось, но последующая бизнес-обработка не удалась, повторяйте её из своей очереди, а не теряйте уже подтверждённое событие. При получении старого события не перезаписывайте более новое состояние слепо: created_at не является версией ресурса; выполните GET. Регулярно сверяйте незавершённые заказы и операции, в том числе при тишине webhook. Поведение Retry-After описано с учётом [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-retry-after); конкретные интервалы, caps и список статусов выше являются контрактом этой реализации, а не требованиями RFC. Политика обработки HTTP-ошибок и ограничения REST описаны в [руководстве надёжности](./reliability.md). --- SOURCE: /index.md # Reseller API REST API для подключения собственного биллинга реселлера: клиенты, контакты владельцев, тарифы, регистрация, продление, перенос и управление доменами, заказы, баланс и уведомления. Базовый путь: `/api/reseller/v1`. Адрес сервера и ключ выдаёт оператор платформы. Наличие этой документации не означает, что API уже включён на конкретном сервере. ## Начать интеграцию 1. [Быстрый старт](guide/quickstart.md): доступ, окружение, первая проверка, подбор прав. 2. [Модель данных](guide/concepts.md): реселлер и клиент, идентификаторы, деньги и расчёты. 3. [Регистрация домена](guide/registration.md): полный процесс от контакта до подтверждённой регистрации. 4. [Управление доменом](guide/domain-management.md): продление, перенос, восстановление, NS, контакты, блокировки и ЭЦП. 5. [Надёжность](guide/reliability.md): ошибки, rate limit, идемпотентность и повторные запросы. 6. [Webhooks](guide/webhooks.md): подпись, повторная доставка и синхронизация состояния. 7. [Исполняемые примеры](guide/examples.md) и [критерии готовности интеграции](guide/integration.md). ## Справочник и загрузки В разделе методов приведены конкретные запросы и ответы, права, параметры, вложенные поля и условия ошибок. Справочник генерируется из того же контракта, который используется для машинных загрузок. - [OpenAPI JSON](/openapi.json) и [OpenAPI YAML](/reseller-api-openapi.yaml). - [Индекс для ИИ](/llms.txt) и [полный текст](/llms-full.txt). - [Правила ведения документации](policy.md). Все UUID, адреса, домены и ключи в примерах иллюстративные. Не отправляйте примеры регистрации в live без замены на согласованные данные: домен из примера может принадлежать третьему лицу. ## Границы API API использует серверные Bearer-ключи, а не сессию личного кабинета. Реселлер работает только со своими объектами и своим окружением. Поддержка конкретной операции зависит от зоны и регистратора: проверяйте `GET /domains/{domainUuid}/capabilities`. Управление NS и glue-записями не является управлением DNS-зоной: методов A, MX, TXT или CNAME здесь нет. Поддержку DNSSEC нельзя выводить из наличия EPP: отдельного публичного метода для него в этой версии нет. `POST /hosting/orders` позволяет заказать хостинг, но не обещает полный API управления хостинговой панелью. HTTP `201` при создании заказа и `202` при постановке операции не подтверждают выполнение команды в реестре. Результат отслеживается отдельно. При неизвестном результате нельзя создавать новую платную операцию, пока не выяснен исход предыдущей. --- SOURCE: /policy.md # Правила документации Эти правила применяются к Reseller API v1. Документация описывает реализованный контракт, а не желаемое поведение. Основа проверки: маршруты, middleware, валидаторы, ресурсы ответов, сервисы операций и тесты текущего релиза. Поведение конкретного реестра и настройки оператора указываются отдельно. ## Использованные рекомендации | Источник | Что применяем | | --- | --- | | [Diátaxis](https://www.diataxis.fr/start-here/) | Разделяем учебный сценарий, практические инструкции, справочник и объяснение модели. | | [Google AIP-192](https://google.aip.dev/192) | Описываем каждый метод и поле, допустимые значения, единицы, значения по умолчанию, побочные эффекты и ошибки. | | [Google API reference guidance](https://developers.google.com/style/api-reference-comments) | Указываем назначение, параметры, результат, исключения и рабочие примеры. | | [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1) | Один структурированный контракт для схем, примеров, страниц методов и машинных клиентов. | | [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html) | Не смешиваем HTTP-успех с завершением бизнес-операции. | | [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585.html) | Описываем HTTP 429 и фактическое использование Retry-After. | | [llms.txt](https://llmstxt.org/) | Предоставляем индекс и Markdown для обработки ИИ. Это предложение формата, не официальный стандарт совместимости ИИ. | Принципы AIP-192 адаптированы к русскоязычному REST API. Это не заявление о сертификации Google или полном соответствии protobuf-правилам. Формат ошибок платформы собственный; он не объявляется RFC 9457 Problem Details. ## Обязательный состав метода - HTTP-метод и путь, назначение, доступные окружения и необходимые scopes. - Параметры пути, query, headers и тела: тип, обязательность, nullable, длина, диапазон, enum, значение по умолчанию и условные зависимости. - Успешные ответы с полным примером и расшифровкой вложенных полей. - Применимые статусы ошибок, стабильный код, условие возникновения и действие клиента. - Требования к Idempotency-Key, повтору, асинхронному завершению и финансовым последствиям. - Ограничения регистратора и условия, зависящие от настроек, без обещания недоступных возможностей. «Все ответы» означает все документируемые ветки HTTP/бизнес-контракта и типы результатов операций. Тексты сообщений, произвольные сообщения внешнего реестра и ошибки reverse proxy не являются конечным перечислимым набором. Клиент обязан иметь обработчик неизвестного кода, статуса и не-JSON ответа. ## Проверки изменений Спецификация собирается из тематических YAML-фрагментов. Страницы методов не редактируются независимо от неё. Изменение запроса или ответа должно сопровождаться изменением схемы, примера, сценария и теста. Перед публикацией проверяются разбор OpenAPI и ссылки `$ref`, соответствие маршрутам, соответствие примеров схемам, ссылки руководств, машинные загрузки, сборка и отображение портала. Исполняемые примеры тестируются на подставном транспорте без операций в реестре. Статические проверки не заменяют проверку интеграции в sandbox. Не считаем наличие примера доказательством принятия произвольного контакта NIC.KZ или доступности домена. Дату релиза API и окружение необходимо согласовывать с оператором независимо от даты публикации портала. ## Совместимость Клиенты должны игнорировать неизвестные дополнительные поля, не разбирать человекочитаемое `message` как машинный код, не считать новый статус успешным по умолчанию и сохранять исходные идентификаторы для диагностики. Удаление или изменение смысла поля требует оценки совместимости и уведомления интеграторов; префикс `/v1` сам по себе не является обещанием неизменности всех внешних реестров. Из документации исключаются реальные токены, персональные данные клиентов, секреты ЭЦП и production-конфигурация. Операторские инструкции по деплою не входят в сценарий подключения реселлера. --- SOURCE: /reference/resellerContext.md # Контекст ключа и реселлера `GET /context` **operationId:** `resellerContext` Дополнительный scope не требуется. Возвращает метаданные текущего ключа, включая среду, scopes и настроенный rate_limit_per_minute. Не возвращает секрет, checkout_mode или остаток средств. Лимит нельзя изменять через этот endpoint. **Scopes:** Дополнительные scopes не требуются; действительный Bearer-ключ обязателен. ## Параметры Параметры пути, строки запроса и дополнительные заголовки отсутствуют. ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/context" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/context"; const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/context" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/context" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/context"; URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/context"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreContextEnvelope | | `$.data` | object | Да | required: ["provider","credential"] | Контекст определяется bearer-ключом, а не параметром provider_uuid. Схема: CoreContext | | `$.data.provider` | object | Да | required: ["uuid","code","name","status","operation_mode","customer_mode","currency","locale","timezone"] | Данные реселлера. | | `$.data.provider.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.provider.code` | string | Да | | Код реселлера. | | `$.data.provider.name` | string | Да | | Название. | | `$.data.provider.status` | string | Да | | Текущий статус реселлера. | | `$.data.provider.operation_mode` | string | Да | | Режим работы платформы. | | `$.data.provider.customer_mode` | string | Да | enum: "managed", "aggregate" | managed требует customer_uuid; aggregate позволяет опустить его. | | `$.data.provider.currency` | string | Да | minLength: 3; maxLength: 3 | Валюта по умолчанию. | | `$.data.provider.locale` | string | Да | | Язык по умолчанию. | | `$.data.provider.timezone` | string | Да | | Часовой пояс IANA. | | `$.data.credential` | object | Да | required: ["uuid","name","environment","scopes","rate_limit_per_minute","expires_at"] | Метаданные текущего ключа; секрет не возвращается. | | `$.data.credential.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.credential.name` | string | Да | | Имя интеграции. | | `$.data.credential.environment` | string | Да | enum: "test", "live" | Изолированная среда. | | `$.data.credential.scopes` | array | Да | | Выданные права. | | `$.data.credential.scopes[]` | string | Да | | Scope. | | `$.data.credential.rate_limit_per_minute` | integer | Да | format: "int64" | Настроенный лимит ключа. Ограничение применяется общим middleware; учитывайте заголовки ответа. | | `$.data.credential.expires_at` | string / null | Да | format: "date-time" | Дата истечения ключа; null означает отсутствие срока. | #### Рабочий ключ (200) ```json { "data": { "provider": { "uuid": "019a0000-0000-7000-8000-000000000001", "code": "example-reseller", "name": "Example Reseller", "status": "active", "operation_mode": "hybrid", "customer_mode": "managed", "currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty" }, "credential": { "uuid": "019a0000-0000-7000-8000-000000000002", "name": "External billing", "environment": "live", "scopes": [ "catalog.read", "quotes.create", "orders.create", "orders.read", "domains.register", "domains.read", "customers.read", "customers.write", "contacts.read", "contacts.write" ], "rate_limit_per_minute": 60, "expires_at": null } } } ``` #### Тестовый ключ (200) ```json { "data": { "provider": { "uuid": "019a0000-0000-7000-8000-000000000001", "code": "example-reseller", "name": "Example Reseller", "status": "active", "operation_mode": "hybrid", "customer_mode": "managed", "currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty" }, "credential": { "uuid": "019a0000-0000-7000-8000-000000000002", "name": "External billing", "environment": "test", "scopes": [ "catalog.read", "quotes.create", "orders.create", "orders.read", "domains.register", "domains.read", "customers.read", "customers.write", "contacts.read", "contacts.write" ], "rate_limit_per_minute": 60, "expires_at": null } } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listProducts.md # Опубликованные продукты `GET /catalog/products` **operationId:** `listProducts` Список provider_offerings со status=published и product_id, по sort_order. uuid относится к предложению; в quotes используйте resource_uuid. Цены не возвращаются: рассчитывайте через POST /quotes. Каталог не проверяет наличие test-интеграции; test заказы хостинга запрещены. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `catalog.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/catalog/products?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/catalog/products"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/catalog/products" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/catalog/products" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/catalog/products"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/catalog/products"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Список и упрощённая пагинация. Схема: CoreProductPage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","resource_uuid","name","allowed_periods","allowed_operations","retail_pricing_mode","code","type","description"] | Опубликованное предложение продукта. Цены в этом ответе отсутствуют. Схема: CoreProduct | | `$.data[].uuid` | string | Да | format: "uuid" | UUID предложения. Не передавайте вместо resource_uuid в quotes. | | `$.data[].resource_uuid` | string / null | Да | format: "uuid" | UUID продукта или зоны для quotes. | | `$.data[].name` | string / null | Да | | Отображаемое название. | | `$.data[].allowed_periods` | array | Да | | Если пусто, предложение не ограничивает периоды; наличие цены всё равно обязательно. | | `$.data[].allowed_periods[]` | object | Да | required: ["unit","count"] | Разрешённый период предложения. Схема: CorePeriod | | `$.data[].allowed_periods[].unit` | string | Да | enum: "day", "month", "year" | Единица. | | `$.data[].allowed_periods[].count` | integer | Да | format: "int64"; minimum: 1 | Количество. | | `$.data[].allowed_operations` | array | Да | | Если пусто, предложение не ограничивает операции; это не означает наличие API для каждой операции. | | `$.data[].allowed_operations[]` | string | Да | | Операция. | | `$.data[].retail_pricing_mode` | string | Да | | Режим расчёта розничной цены; итог определяется quotes. | | `$.data[].code` | string / null | Да | | Код продукта. | | `$.data[].type` | string / null | Да | | Тип продукта, например hosting. | | `$.data[].description` | string / null | Да | | Описание продукта. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Упрощённая курсорная пагинация. total, page и prev_cursor отсутствуют. Схема: CoreCursorMeta | | `$.meta.next_cursor` | string / null | Да | | Передать как cursor следующего запроса. null означает конец. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Фактический размер страницы. | #### Последняя страница продуктов (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000003", "resource_uuid": "019a0000-0000-7000-8000-000000000004", "code": "hosting-standard", "type": "hosting", "name": "Hosting Standard", "description": "Хостинг для одного сайта", "allowed_periods": [ { "unit": "month", "count": 1 } ], "allowed_operations": [ "order" ], "retail_pricing_mode": "provider_fixed" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listDomainZones.md # Опубликованные доменные зоны `GET /catalog/domain-zones` **operationId:** `listDomainZones` Опубликованные предложения с domain_zone_id, по sort_order. Флаги зоны не заменяют проверку capabilities и маршрутизации регистратора. Цены запрашиваются через quotes, resource_uuid означает UUID зоны. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `catalog.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/catalog/domain-zones?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/catalog/domain-zones"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/catalog/domain-zones" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/catalog/domain-zones" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/catalog/domain-zones"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/catalog/domain-zones"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Список и упрощённая пагинация. Схема: CoreZonePage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","resource_uuid","name","allowed_periods","allowed_operations","retail_pricing_mode","zone","registration_enabled","renewal_enabled","transfer_enabled","requires_eds_verification","requires_edu_license","min_registration_years","max_registration_years"] | Опубликованная доменная зона. Каталог общий для ключей, доступность конкретного test/live регистратора проверяется при операции. Схема: CoreZone | | `$.data[].uuid` | string | Да | format: "uuid" | UUID предложения. Не передавайте вместо resource_uuid в quotes. | | `$.data[].resource_uuid` | string / null | Да | format: "uuid" | UUID продукта или зоны для quotes. | | `$.data[].name` | string / null | Да | | Отображаемое название. | | `$.data[].allowed_periods` | array | Да | | Если пусто, предложение не ограничивает периоды; наличие цены всё равно обязательно. | | `$.data[].allowed_periods[]` | object | Да | required: ["unit","count"] | Разрешённый период предложения. Схема: CorePeriod | | `$.data[].allowed_periods[].unit` | string | Да | enum: "day", "month", "year" | Единица. | | `$.data[].allowed_periods[].count` | integer | Да | format: "int64"; minimum: 1 | Количество. | | `$.data[].allowed_operations` | array | Да | | Если пусто, предложение не ограничивает операции; это не означает наличие API для каждой операции. | | `$.data[].allowed_operations[]` | string | Да | | Операция. | | `$.data[].retail_pricing_mode` | string | Да | | Режим расчёта розничной цены; итог определяется quotes. | | `$.data[].zone` | string / null | Да | | Зона без начальной точки, например kz. | | `$.data[].registration_enabled` | boolean / null | Да | | Разрешена регистрация. | | `$.data[].renewal_enabled` | boolean / null | Да | | Разрешено продление. | | `$.data[].transfer_enabled` | boolean / null | Да | | Разрешён трансфер. | | `$.data[].requires_eds_verification` | boolean / null | Да | | Требуется проверка ЭЦП. | | `$.data[].requires_edu_license` | boolean / null | Да | | Требуется образовательная лицензия. | | `$.data[].min_registration_years` | integer / null | Да | | Минимальный срок регистрации. | | `$.data[].max_registration_years` | integer / null | Да | | Максимальный срок регистрации. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Упрощённая курсорная пагинация. total, page и prev_cursor отсутствуют. Схема: CoreCursorMeta | | `$.meta.next_cursor` | string / null | Да | | Передать как cursor следующего запроса. null означает конец. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Фактический размер страницы. | #### Последняя страница зон (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000005", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "zone": "kz", "name": ".kz", "registration_enabled": true, "renewal_enabled": true, "transfer_enabled": true, "requires_eds_verification": false, "requires_edu_license": false, "min_registration_years": 1, "max_registration_years": 10, "allowed_periods": [ { "unit": "year", "count": 1 } ], "allowed_operations": [ "register", "renew", "transfer", "restore" ], "retail_pricing_mode": "provider_fixed" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createQuote.md # Рассчитать и зафиксировать котировку `POST /quotes` **operationId:** `createQuote` Котировка фиксирует цену и payload, но не резервирует доменное имя и не означает исполнение. Срок по умолчанию 15 минут, источник истины expires_at. Исполнять должен тот же bearer-клиент, тот же customer и environment. Нельзя передать свою цену, скидку или сумму. GET/DELETE котировки и изменение выданной котировки не реализованы. Для доменных renew/restore используйте target UUID; restore проверяет период восстановления даже для локально удалённого домена. Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. **Scopes:** `quotes.create` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["items"] | Котировка привязывается к текущему ключу, реселлеру, покупателю и среде. Срок задаётся сервером, по умолчанию 15 минут; используйте expires_at. Создание котировки не регистрирует домен и не резервирует имя. Схема: CoreCreateQuoteRequest | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Обязателен в managed; aggregate допускает отсутствие. | | `$.items` | array | Да | minItems: 1; maxItems: 100 | Строки котировки. | | `$.items[]` | object | Да | required: ["resource_type","resource_uuid","operation","period_unit","period_count"]; Дополнительные поля запрещены | Цена выводится из опубликованного предложения, тарифного плана и правил реселлера. Схема: CoreQuoteInputItem | | `$.items[].resource_type` | string | Да | enum: "product", "domain_zone" | Тип ресурса. | | `$.items[].resource_uuid` | string | Да | format: "uuid" | resource_uuid из каталога; не uuid предложения. | | `$.items[].operation` | string | Да | enum: "order", "add-on", "register", "renew", "transfer", "restore" | Допустимые значения валидатора. order/register исполняются через orders; renew/transfer/restore через доменные методы. Наличие add-on в валидаторе НЕ означает отдельный метод исполнения add-on. | | `$.items[].period_unit` | string | Да | enum: "day", "month", "year" | Для доменов только year. | | `$.items[].period_count` | integer | Да | format: "int64"; minimum: 1; maximum: 120 | От 1 до 120 на уровне quote. Регистрация домена дополнительно ограничена 1..10 годами; transfer/restore используют 1 год. | | `$.items[].quantity` | integer | Нет | format: "int64"; minimum: 1; maximum: 100; default: 1 | По умолчанию 1. При регистрации/платных доменных операциях ровно 1. | | `$.items[].currency` | string | Нет | minLength: 3; maxLength: 3 | Код длиной 3. Валюта всей котировки выбирается из первой строки либо настроек реселлера; currency последующих строк не переключает валюту. Передавайте одинаковую валюту во всех строках. | | `$.items[].payload` | object | Нет | required: []; Дополнительные поля разрешены | Payload сохраняется в котировке. Для register обязательность domain_name/contact_uuids и ограничения NS проверяются при создании заказа, а не только quotes. Для renew/restore укажите domain_service_uuid (domain_uuid поддерживается как альтернативное имя). Для product/order передавайте domain_name для хостинга. Неизвестные поля могут храниться, но не получают автоматической семантики. Для product/order этот контракт гарантирует только поля, реально используемые OrderCreator: domain_name, notes, external_id. Произвольный payload не передаётся автоматически как команда Plesk. Схема: CoreQuotePayload | | `$.items[].payload.domain_name` | string | Нет | maxLength: 253 | Домен, соответствующий зоне; для hosting домен сайта. Для хостинга отсутствие/ошибка домена может проявиться при асинхронном provisioning, даже если quote/заказ приняты. | | `$.items[].payload.contact_uuids` | object | Нет | required: ["owner","admin","tech","billing"] | Все четыре роли обязательны при регистрации; допустим один UUID для нескольких ролей. Схема: CoreContactRoles | | `$.items[].payload.contact_uuids.owner` | string | Да | format: "uuid" | Владелец. | | `$.items[].payload.contact_uuids.admin` | string | Да | format: "uuid" | Административный контакт. | | `$.items[].payload.contact_uuids.tech` | string | Да | format: "uuid" | Технический контакт. | | `$.items[].payload.contact_uuids.billing` | string | Да | format: "uuid" | Платёжный контакт. | | `$.items[].payload.nameservers` | array | Нет | minItems: 2; maxItems: 6 | Необязательно при регистрации; при наличии от 2 до 6, hostname уникальны без учёта регистра. | | `$.items[].payload.nameservers[]` | object | Да | required: ["hostname"] | NS домена; IP передаются при необходимости glue. Схема: CoreNameserver | | `$.items[].payload.nameservers[].hostname` | string | Да | maxLength: 253 | Имя NS. | | `$.items[].payload.nameservers[].ipv4` | string / null | Нет | format: "ipv4" | IPv4. | | `$.items[].payload.nameservers[].ipv6` | string / null | Нет | format: "ipv6" | IPv6. | | `$.items[].payload.purpose` | string / null | Нет | maxLength: 1000 | Назначение домена. | | `$.items[].payload.notes` | string / null | Нет | maxLength: 1000 | Примечание заказа. | | `$.items[].payload.external_id` | string / null | Нет | maxLength: 255 | Идентификатор создаваемой услуги, уникален для реселлера+среды. | | `$.items[].payload.whois_privacy_enabled` | boolean | Нет | | Запрос приватности при регистрации; поддержка зависит от регистратора. | | `$.items[].payload.domain_service_uuid` | string | Нет | format: "uuid" | UUID домена для операции над существующим доменом. | | `$.items[].payload.domain_uuid` | string | Нет | format: "uuid" | Альтернативное имя domain_service_uuid. | #### Регистрация ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "register", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "reseller-example.kz", "external_id": "domain-3042", "contact_uuids": { "owner": "019a0000-0000-7000-8000-000000000012", "admin": "019a0000-0000-7000-8000-000000000012", "tech": "019a0000-0000-7000-8000-000000000012", "billing": "019a0000-0000-7000-8000-000000000012" }, "nameservers": [ { "hostname": "ns1.example.com" }, { "hostname": "ns2.example.com" } ], "purpose": "Сайт организации", "whois_privacy_enabled": false } } ] } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/quotes" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "register", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "reseller-example.kz", "external_id": "domain-3042", "contact_uuids": { "owner": "019a0000-0000-7000-8000-000000000012", "admin": "019a0000-0000-7000-8000-000000000012", "tech": "019a0000-0000-7000-8000-000000000012", "billing": "019a0000-0000-7000-8000-000000000012" }, "nameservers": [ { "hostname": "ns1.example.com" }, { "hostname": "ns2.example.com" } ], "purpose": "Сайт организации", "whois_privacy_enabled": false } } ] }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/quotes"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "register", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "reseller-example.kz", "external_id": "domain-3042", "contact_uuids": { "owner": "019a0000-0000-7000-8000-000000000012", "admin": "019a0000-0000-7000-8000-000000000012", "tech": "019a0000-0000-7000-8000-000000000012", "billing": "019a0000-0000-7000-8000-000000000012" }, "nameservers": [ { "hostname": "ns1.example.com" }, { "hostname": "ns2.example.com" } ], "purpose": "Сайт организации", "whois_privacy_enabled": false } } ] }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/quotes" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"register\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"reseller-example.kz\",\n \"external_id\": \"domain-3042\",\n \"contact_uuids\": {\n \"owner\": \"019a0000-0000-7000-8000-000000000012\",\n \"admin\": \"019a0000-0000-7000-8000-000000000012\",\n \"tech\": \"019a0000-0000-7000-8000-000000000012\",\n \"billing\": \"019a0000-0000-7000-8000-000000000012\"\n },\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.com\"\n },\n {\n \"hostname\": \"ns2.example.com\"\n }\n ],\n \"purpose\": \"Сайт организации\",\n \"whois_privacy_enabled\": false\n }\n }\n ]\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/quotes" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"register\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"reseller-example.kz\",\n \"external_id\": \"domain-3042\",\n \"contact_uuids\": {\n \"owner\": \"019a0000-0000-7000-8000-000000000012\",\n \"admin\": \"019a0000-0000-7000-8000-000000000012\",\n \"tech\": \"019a0000-0000-7000-8000-000000000012\",\n \"billing\": \"019a0000-0000-7000-8000-000000000012\"\n },\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.com\"\n },\n {\n \"hostname\": \"ns2.example.com\"\n }\n ],\n \"purpose\": \"Сайт организации\",\n \"whois_privacy_enabled\": false\n }\n }\n ]\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/quotes"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"register\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"reseller-example.kz\",\n \"external_id\": \"domain-3042\",\n \"contact_uuids\": {\n \"owner\": \"019a0000-0000-7000-8000-000000000012\",\n \"admin\": \"019a0000-0000-7000-8000-000000000012\",\n \"tech\": \"019a0000-0000-7000-8000-000000000012\",\n \"billing\": \"019a0000-0000-7000-8000-000000000012\"\n },\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.com\"\n },\n {\n \"hostname\": \"ns2.example.com\"\n }\n ],\n \"purpose\": \"Сайт организации\",\n \"whois_privacy_enabled\": false\n }\n }\n ]\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/quotes"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"register\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"reseller-example.kz\",\n \"external_id\": \"domain-3042\",\n \"contact_uuids\": {\n \"owner\": \"019a0000-0000-7000-8000-000000000012\",\n \"admin\": \"019a0000-0000-7000-8000-000000000012\",\n \"tech\": \"019a0000-0000-7000-8000-000000000012\",\n \"billing\": \"019a0000-0000-7000-8000-000000000012\"\n },\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.com\"\n },\n {\n \"hostname\": \"ns2.example.com\"\n }\n ],\n \"purpose\": \"Сайт организации\",\n \"whois_privacy_enabled\": false\n }\n }\n ]\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Продление ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "renew", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a0000-0000-7000-8000-000000000041" } } ] } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/quotes" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "renew", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a0000-0000-7000-8000-000000000041" } } ] }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/quotes"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "renew", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a0000-0000-7000-8000-000000000041" } } ] }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/quotes" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"renew\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/quotes" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"renew\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/quotes"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"renew\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/quotes"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"renew\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Восстановление ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "restore", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a0000-0000-7000-8000-000000000041" } } ] } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/quotes" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "restore", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a0000-0000-7000-8000-000000000041" } } ] }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/quotes"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a0000-0000-7000-8000-000000000006", "operation": "restore", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a0000-0000-7000-8000-000000000041" } } ] }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/quotes" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"restore\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/quotes" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"restore\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/quotes"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"restore\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/quotes"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"domain_zone\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000006\",\n \"operation\": \"restore\",\n \"period_unit\": \"year\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_service_uuid\": \"019a0000-0000-7000-8000-000000000041\"\n }\n }\n ]\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Заказ хостинга (только live при исполнении) ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "product", "resource_uuid": "019a0000-0000-7000-8000-000000000004", "operation": "order", "period_unit": "month", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "www.example.com", "external_id": "hosting-3043", "notes": "Основной сайт" } } ] } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/quotes" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "product", "resource_uuid": "019a0000-0000-7000-8000-000000000004", "operation": "order", "period_unit": "month", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "www.example.com", "external_id": "hosting-3043", "notes": "Основной сайт" } } ] }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/quotes"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "items": [ { "resource_type": "product", "resource_uuid": "019a0000-0000-7000-8000-000000000004", "operation": "order", "period_unit": "month", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "www.example.com", "external_id": "hosting-3043", "notes": "Основной сайт" } } ] }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/quotes" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"product\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000004\",\n \"operation\": \"order\",\n \"period_unit\": \"month\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"www.example.com\",\n \"external_id\": \"hosting-3043\",\n \"notes\": \"Основной сайт\"\n }\n }\n ]\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/quotes" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"product\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000004\",\n \"operation\": \"order\",\n \"period_unit\": \"month\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"www.example.com\",\n \"external_id\": \"hosting-3043\",\n \"notes\": \"Основной сайт\"\n }\n }\n ]\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/quotes"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"product\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000004\",\n \"operation\": \"order\",\n \"period_unit\": \"month\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"www.example.com\",\n \"external_id\": \"hosting-3043\",\n \"notes\": \"Основной сайт\"\n }\n }\n ]\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/quotes"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"items\": [\n {\n \"resource_type\": \"product\",\n \"resource_uuid\": \"019a0000-0000-7000-8000-000000000004\",\n \"operation\": \"order\",\n \"period_unit\": \"month\",\n \"period_count\": 1,\n \"quantity\": 1,\n \"currency\": \"KZT\",\n \"payload\": {\n \"domain_name\": \"www.example.com\",\n \"external_id\": \"hosting-3043\",\n \"notes\": \"Основной сайт\"\n }\n }\n ]\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Модуль pricing отключён либо ресурс/домен восстановления недоступен. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже занят; либо домен восстановления использует неактивную интеграцию или имеет migration_hold. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Нет активного тарифного назначения, опубликованного предложения, цены нужного периода/операции/валюты; ограничения тарифа; неверный customer_uuid; домен вне периода восстановления. PriceUnavailableException оборачивается как validation_failed, а не отдельный error.code=price_unavailable. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreQuoteEnvelope | | `$.data` | object | Да | required: ["uuid","customer_uuid","status","environment","currency","issued_at","expires_at","items","total_minor"] | Выданная котировка. Нет GET /quotes/{uuid}; сохраняйте этот ответ. Схема: CoreQuote | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.customer_uuid` | string | Да | format: "uuid" | Покупатель. | | `$.data.status` | string | Да | | При создании active. Позже котировка может стать consumed/expired; отдельного GET нет. | | `$.data.environment` | string | Да | enum: "test", "live" | Среда. | | `$.data.currency` | string | Да | | Валюта. | | `$.data.issued_at` | string | Да | format: "date-time" | Выдана. | | `$.data.expires_at` | string | Да | format: "date-time" | Истекает. | | `$.data.items` | array | Да | | Строки. | | `$.data.items[]` | object | Да | required: ["uuid","resource_type","operation","period_unit","period_count","quantity","currency","unit_price_minor","subtotal_minor","discount_minor","tax_minor","total_minor"] | Розничные суммы строки. Закупочная цена, себестоимость и resource_uuid не возвращаются. Схема: CoreQuoteItem | | `$.data.items[].uuid` | string | Да | format: "uuid" | UUID строки котировки. | | `$.data.items[].resource_type` | string | Да | | Тип ресурса. | | `$.data.items[].operation` | string | Да | | Операция. | | `$.data.items[].period_unit` | string | Да | | Единица периода. | | `$.data.items[].period_count` | integer | Да | format: "int64" | Количество периодов. | | `$.data.items[].quantity` | integer | Да | format: "int64" | Количество. | | `$.data.items[].currency` | string | Да | | Валюта. | | `$.data.items[].unit_price_minor` | integer | Да | format: "int64" | Цена единицы. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].subtotal_minor` | integer | Да | format: "int64" | Подытог. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].discount_minor` | integer | Да | format: "int64" | Скидка. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].tax_minor` | integer | Да | format: "int64" | Налог. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].total_minor` | integer | Да | format: "int64" | Итого строки. В минимальных единицах валюты, без десятичных дробей. | | `$.data.total_minor` | integer | Да | format: "int64" | Сумма total_minor всех строк. В минимальных единицах валюты, без десятичных дробей. | #### Регистрация на год (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "status": "active", "environment": "live", "currency": "KZT", "issued_at": "2026-10-05T09:00:00+00:00", "expires_at": "2026-10-05T09:15:00+00:00", "items": [ { "uuid": "019a0000-0000-7000-8000-000000000021", "resource_type": "domain_zone", "operation": "register", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "unit_price_minor": 700000, "subtotal_minor": 700000, "discount_minor": 0, "tax_minor": 0, "total_minor": 700000 } ], "total_minor": 700000 } } ``` #### Продление на год (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "status": "active", "environment": "live", "currency": "KZT", "issued_at": "2026-10-05T09:00:00+00:00", "expires_at": "2026-10-05T09:15:00+00:00", "items": [ { "uuid": "019a0000-0000-7000-8000-000000000021", "resource_type": "domain_zone", "operation": "renew", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "unit_price_minor": 700000, "subtotal_minor": 700000, "discount_minor": 0, "tax_minor": 0, "total_minor": 700000 } ], "total_minor": 700000 } } ``` #### Хостинг на месяц (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "status": "active", "environment": "live", "currency": "KZT", "issued_at": "2026-10-05T09:00:00+00:00", "expires_at": "2026-10-05T09:15:00+00:00", "items": [ { "uuid": "019a0000-0000-7000-8000-000000000021", "resource_type": "product", "operation": "order", "period_unit": "month", "period_count": 1, "quantity": 1, "currency": "KZT", "unit_price_minor": 700000, "subtotal_minor": 700000, "discount_minor": 0, "tax_minor": 0, "total_minor": 700000 } ], "total_minor": 700000 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Модуль pricing отключён либо ресурс/домен восстановления недоступен. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт Idempotency-Key; для restore также неактивная интеграция домена или блокировка migration_hold. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Нет активного тарифного назначения, опубликованного предложения, цены нужного периода/операции/валюты; ограничения тарифа; неверный customer_uuid; домен вне периода восстановления. PriceUnavailableException оборачивается как validation_failed, а не отдельный error.code=price_unavailable. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listOrders.md # Список заказов `GET /orders` **operationId:** `listOrders` Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. Сортировка: id по убыванию. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `orders.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ### customer_uuid Расположение: `query`. Необязательный. Фильтр по UUID покупателя. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### external_id Расположение: `query`. Необязательный. Точное совпадение ID заказа внешнего биллинга. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### status Расположение: `query`. Необязательный. Точное совпадение статуса; неизвестное значение обычно даёт пустой список. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/orders?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/orders"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/orders" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/orders" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/orders"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/orders"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","links","meta"] | Список и ресурсная курсорная пагинация. Схема: CoreOrderPage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","external_id","customer_uuid","number","status","currency","total_minor","submitted_at","paid_at","completed_at","failed_at","failure_reason","items","invoice","created_at","updated_at"] | Заказ принят не равнозначно домен зарегистрирован. В external checkout выставляется оплаченный счёт и планируется асинхронное исполнение; дальнейшее состояние отслеживайте через заказ/услугу и webhooks. Схема: CoreOrder | | `$.data[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].external_id` | string / null | Да | | ID заказа внешнего биллинга. | | `$.data[].customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data[].number` | string | Да | | Номер заказа. | | `$.data[].status` | string | Да | enum: "draft", "submitted", "payment_authorized", "registration_processing", "registry_pending", "paid", "active", "completed", "registration_failed", "cancelled", "refunded" | Состояние заказа. submitted: выставлен счёт; paid: оплата зафиксирована; registration_processing/registry_pending: работа с реестром; active/completed: исполнен; registration_failed: регистрация неуспешна. Даты и invoice отражают отдельные стадии, HTTP 201 не терминальный статус. | | `$.data[].currency` | string | Да | | Валюта. | | `$.data[].total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].submitted_at` | string / null | Да | format: "date-time" | Подан. | | `$.data[].paid_at` | string / null | Да | format: "date-time" | Оплачен. | | `$.data[].completed_at` | string / null | Да | format: "date-time" | Завершён. | | `$.data[].failed_at` | string / null | Да | format: "date-time" | Ошибка. | | `$.data[].failure_reason` | string / null | Да | | Причина ошибки; не используйте текст как стабильный машинный код. | | `$.data[].items` | array | Да | | Строки. | | `$.data[].items[]` | object | Да | required: ["uuid","type","name","quantity","unit_price_minor","total_minor","currency","service_uuid","service_external_id"] | Строка заказа. Схема: CoreOrderItem | | `$.data[].items[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].items[].type` | string | Да | | Тип услуги, например domain/hosting. | | `$.data[].items[].name` | string | Да | | Название. | | `$.data[].items[].quantity` | integer | Да | format: "int64" | Количество. | | `$.data[].items[].unit_price_minor` | integer | Да | format: "int64" | Цена единицы. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].items[].total_minor` | integer | Да | format: "int64" | Итого. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].items[].currency` | string | Да | | Валюта. | | `$.data[].items[].service_uuid` | string / null | Да | format: "uuid" | UUID созданной услуги. Отличается от domain UUID. | | `$.data[].items[].service_external_id` | string / null | Да | | external_id из payload строки котировки. | | `$.data[].invoice` | anyOf | Да | | Счёт или null. | | `$.data[].invoice (anyOf 1)` | object | Да | required: ["uuid","number","status","currency","total_minor","paid_minor","payment_collected_by_platform"] | Связанный счёт. Схема: CoreInvoice | | `$.data[].invoice (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].invoice (anyOf 1).number` | string | Да | | Номер. | | `$.data[].invoice (anyOf 1).status` | string | Да | | Состояние счёта, например issued/paid/cancelled. | | `$.data[].invoice (anyOf 1).currency` | string | Да | | Валюта. | | `$.data[].invoice (anyOf 1).total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].invoice (anyOf 1).paid_minor` | integer | Да | format: "int64" | Оплачено. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].invoice (anyOf 1).payment_collected_by_platform` | boolean | Да | | false при external checkout: оплату клиента принимает реселлер. paid не означает списание с карты платформой. | | `$.data[].invoice (anyOf 2)` | null | Да | | | | `$.data[].created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data[].updated_at` | string / null | Да | format: "date-time" | Изменён. | | `$.links` | object | Да | required: ["first","last","prev","next"] | Навигационные ссылки. Курсорная пагинация не вычисляет первую/последнюю страницу. Сохраняйте исходные фильтры при переходе. Схема: CoreResourceLinks | | `$.links.first` | null | Да | | Всегда null. | | `$.links.last` | null | Да | | Всегда null. | | `$.links.prev` | string / null | Да | format: "uri" | Предыдущая страница или null. | | `$.links.next` | string / null | Да | format: "uri" | Следующая страница или null. | | `$.meta` | object | Да | required: ["path","per_page","next_cursor","prev_cursor"] | Пагинация Laravel Resource. Не содержит общего количества записей. Схема: CoreResourceMeta | | `$.meta.path` | string | Да | format: "uri" | URL коллекции. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Размер страницы. | | `$.meta.next_cursor` | string / null | Да | | Непрозрачный курсор следующей страницы. | | `$.meta.prev_cursor` | string / null | Да | | Непрозрачный курсор предыдущей страницы. | #### Заказы в текущей среде (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "submitted", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": null, "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "issued", "currency": "KZT", "total_minor": 700000, "paid_minor": 0, "payment_collected_by_platform": true }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" }, { "uuid": "019a0000-0000-7000-8000-000000000033", "external_id": "order-2043", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO43", "status": "registration_failed", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": "2026-10-05T09:00:00+00:00", "completed_at": null, "failed_at": "2026-10-05T09:03:00+00:00", "failure_reason": "Domain registration was rejected by the registrar.", "items": [ { "uuid": "019a0000-0000-7000-8000-000000000034", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000044", "service_external_id": "domain-3043" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000035", "number": "INV-20261005090000-DEMO43", "status": "paid", "currency": "KZT", "total_minor": 700000, "paid_minor": 700000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:03:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.com/api/reseller/v1/orders", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.com/api/reseller/v1/orders", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createOrder.md # Создать заказ по котировке `POST /orders` **operationId:** `createOrder` Исполняет строки order/register котировки. Для доменных строк дополнительно нужен domains.register; для hosting нужен hosting.order. Domain quantity=1, year, 1..10; все четыре контакта должны быть active у того же покупателя и в той же среде. test не поддерживает product-заказы. Для external checkout счёт помечается paid (payment_collected_by_platform=false), исполнение асинхронное; для platform checkout создание заказа не принимает оплату через этот REST API. Итоговые ошибки реестра отслеживайте по заказу/услуге/webhooks. Результат HTTP 201 не гарантирует регистрацию. Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. **Scopes:** `orders.create` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid"] | Создаёт заказ по ранее полученной котировке, а не по произвольной цене. Тот же ключ, покупатель и среда обязательны. Поддерживаются только operation=order/register. Тело одинаково для /orders, /domains и /hosting/orders; последние два маршрута добавляют scopes, но не фильтруют типы строк. Схема: CoreCreateOrderRequest | | `$.quote_uuid` | string | Да | format: "uuid" | UUID неистёкшей, неиспользованной котировки текущего ключа. | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Покупатель котировки; обязателен в managed. | | `$.external_id` | string / null | Нет | maxLength: 255 | ID заказа внешнего биллинга; уникален у реселлера внутри test/live. | #### Заказ для управляемого покупателя ```json { "quote_uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "order-2042" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/orders" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "order-2042" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/orders"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "order-2042" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/orders" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/orders" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/orders"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/orders"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Для агрегированного покупателя ```json { "quote_uuid": "019a0000-0000-7000-8000-000000000020", "external_id": "order-2042" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/orders" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "external_id": "order-2042" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/orders"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "external_id": "order-2042" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/orders" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/orders" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/orders"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/orders"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Не хватает orders.create, domains.register для доменных строк или hosting.order для hosting-строк. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Котировка не найдена у текущего ключа/покупателя/реселлера. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Котировка истекла/использована, другая среда, неподдерживаемая операция; test с product; занятый/дублирующий домен; отсутствующие/чужие контакты; несоответствие зоны; повторный external_id; у покупателя нет владельца для заказа. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreOrderEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","status","currency","total_minor","submitted_at","paid_at","completed_at","failed_at","failure_reason","items","invoice","created_at","updated_at"] | Заказ принят не равнозначно домен зарегистрирован. В external checkout выставляется оплаченный счёт и планируется асинхронное исполнение; дальнейшее состояние отслеживайте через заказ/услугу и webhooks. Схема: CoreOrder | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID заказа внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data.number` | string | Да | | Номер заказа. | | `$.data.status` | string | Да | enum: "draft", "submitted", "payment_authorized", "registration_processing", "registry_pending", "paid", "active", "completed", "registration_failed", "cancelled", "refunded" | Состояние заказа. submitted: выставлен счёт; paid: оплата зафиксирована; registration_processing/registry_pending: работа с реестром; active/completed: исполнен; registration_failed: регистрация неуспешна. Даты и invoice отражают отдельные стадии, HTTP 201 не терминальный статус. | | `$.data.currency` | string | Да | | Валюта. | | `$.data.total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.submitted_at` | string / null | Да | format: "date-time" | Подан. | | `$.data.paid_at` | string / null | Да | format: "date-time" | Оплачен. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Завершён. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Ошибка. | | `$.data.failure_reason` | string / null | Да | | Причина ошибки; не используйте текст как стабильный машинный код. | | `$.data.items` | array | Да | | Строки. | | `$.data.items[]` | object | Да | required: ["uuid","type","name","quantity","unit_price_minor","total_minor","currency","service_uuid","service_external_id"] | Строка заказа. Схема: CoreOrderItem | | `$.data.items[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.items[].type` | string | Да | | Тип услуги, например domain/hosting. | | `$.data.items[].name` | string | Да | | Название. | | `$.data.items[].quantity` | integer | Да | format: "int64" | Количество. | | `$.data.items[].unit_price_minor` | integer | Да | format: "int64" | Цена единицы. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].total_minor` | integer | Да | format: "int64" | Итого. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].currency` | string | Да | | Валюта. | | `$.data.items[].service_uuid` | string / null | Да | format: "uuid" | UUID созданной услуги. Отличается от domain UUID. | | `$.data.items[].service_external_id` | string / null | Да | | external_id из payload строки котировки. | | `$.data.invoice` | anyOf | Да | | Счёт или null. | | `$.data.invoice (anyOf 1)` | object | Да | required: ["uuid","number","status","currency","total_minor","paid_minor","payment_collected_by_platform"] | Связанный счёт. Схема: CoreInvoice | | `$.data.invoice (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.invoice (anyOf 1).number` | string | Да | | Номер. | | `$.data.invoice (anyOf 1).status` | string | Да | | Состояние счёта, например issued/paid/cancelled. | | `$.data.invoice (anyOf 1).currency` | string | Да | | Валюта. | | `$.data.invoice (anyOf 1).total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).paid_minor` | integer | Да | format: "int64" | Оплачено. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).payment_collected_by_platform` | boolean | Да | | false при external checkout: оплату клиента принимает реселлер. paid не означает списание с карты платформой. | | `$.data.invoice (anyOf 2)` | null | Да | | | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Счёт ожидает оплаты (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "submitted", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": null, "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "issued", "currency": "KZT", "total_minor": 700000, "paid_minor": 0, "payment_collected_by_platform": true }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Оплата принимается реселлером, исполнение ещё впереди (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": "2026-10-05T09:00:00+00:00", "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "paid_minor": 700000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Не хватает orders.create, domains.register для доменных строк или hosting.order для hosting-строк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Котировка не найдена у текущего ключа/покупателя/реселлера. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Котировка истекла/использована, другая среда, неподдерживаемая операция; test с product; занятый/дублирующий домен; отсутствующие/чужие контакты; несоответствие зоны; повторный external_id; у покупателя нет владельца для заказа. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createHostingOrder.md # Создать заказ хостинга `POST /hosting/orders` **operationId:** `createHostingOrder` Алиас контроллера /orders с дополнительным scope hosting.order. Сначала получите product/order quote с доменом сайта в payload.domain_name. Только live: test с product даёт 422. Контроллер не ограничивает алиас только hosting-строками; интеграция должна сама выбрать hosting-продукт. HTTP 201 не означает, что Plesk уже создал аккаунт: проверяйте GET /services/{serviceUuid}. Нет reseller REST методов управления панелью, отправки пароля, смены тарифа или продления хостинга. Исполняет строки order/register котировки. Для доменных строк дополнительно нужен domains.register; для hosting нужен hosting.order. Domain quantity=1, year, 1..10; все четыре контакта должны быть active у того же покупателя и в той же среде. test не поддерживает product-заказы. Для external checkout счёт помечается paid (payment_collected_by_platform=false), исполнение асинхронное; для platform checkout создание заказа не принимает оплату через этот REST API. Итоговые ошибки реестра отслеживайте по заказу/услуге/webhooks. Результат HTTP 201 не гарантирует регистрацию. Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. **Scopes:** `orders.create`, `hosting.order` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid"] | Создаёт заказ по ранее полученной котировке, а не по произвольной цене. Тот же ключ, покупатель и среда обязательны. Поддерживаются только operation=order/register. Тело одинаково для /orders, /domains и /hosting/orders; последние два маршрута добавляют scopes, но не фильтруют типы строк. Схема: CoreCreateOrderRequest | | `$.quote_uuid` | string | Да | format: "uuid" | UUID неистёкшей, неиспользованной котировки текущего ключа. | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Покупатель котировки; обязателен в managed. | | `$.external_id` | string / null | Нет | maxLength: 255 | ID заказа внешнего биллинга; уникален у реселлера внутри test/live. | #### Заказ для управляемого покупателя ```json { "quote_uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "order-2042" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/hosting/orders" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "order-2042" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/hosting/orders"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "order-2042" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/hosting/orders" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/hosting/orders" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/hosting/orders"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/hosting/orders"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"order-2042\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Для агрегированного покупателя ```json { "quote_uuid": "019a0000-0000-7000-8000-000000000020", "external_id": "order-2042" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/hosting/orders" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "external_id": "order-2042" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/hosting/orders"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a0000-0000-7000-8000-000000000020", "external_id": "order-2042" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/hosting/orders" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/hosting/orders" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/hosting/orders"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/hosting/orders"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a0000-0000-7000-8000-000000000020\",\n \"external_id\": \"order-2042\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Не хватает orders.create, domains.register для доменных строк или hosting.order для hosting-строк. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Котировка не найдена у текущего ключа/покупателя/реселлера. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Котировка истекла/использована, другая среда, неподдерживаемая операция; test с product; занятый/дублирующий домен; отсутствующие/чужие контакты; несоответствие зоны; повторный external_id; у покупателя нет владельца для заказа. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreOrderEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","status","currency","total_minor","submitted_at","paid_at","completed_at","failed_at","failure_reason","items","invoice","created_at","updated_at"] | Заказ принят не равнозначно домен зарегистрирован. В external checkout выставляется оплаченный счёт и планируется асинхронное исполнение; дальнейшее состояние отслеживайте через заказ/услугу и webhooks. Схема: CoreOrder | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID заказа внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data.number` | string | Да | | Номер заказа. | | `$.data.status` | string | Да | enum: "draft", "submitted", "payment_authorized", "registration_processing", "registry_pending", "paid", "active", "completed", "registration_failed", "cancelled", "refunded" | Состояние заказа. submitted: выставлен счёт; paid: оплата зафиксирована; registration_processing/registry_pending: работа с реестром; active/completed: исполнен; registration_failed: регистрация неуспешна. Даты и invoice отражают отдельные стадии, HTTP 201 не терминальный статус. | | `$.data.currency` | string | Да | | Валюта. | | `$.data.total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.submitted_at` | string / null | Да | format: "date-time" | Подан. | | `$.data.paid_at` | string / null | Да | format: "date-time" | Оплачен. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Завершён. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Ошибка. | | `$.data.failure_reason` | string / null | Да | | Причина ошибки; не используйте текст как стабильный машинный код. | | `$.data.items` | array | Да | | Строки. | | `$.data.items[]` | object | Да | required: ["uuid","type","name","quantity","unit_price_minor","total_minor","currency","service_uuid","service_external_id"] | Строка заказа. Схема: CoreOrderItem | | `$.data.items[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.items[].type` | string | Да | | Тип услуги, например domain/hosting. | | `$.data.items[].name` | string | Да | | Название. | | `$.data.items[].quantity` | integer | Да | format: "int64" | Количество. | | `$.data.items[].unit_price_minor` | integer | Да | format: "int64" | Цена единицы. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].total_minor` | integer | Да | format: "int64" | Итого. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].currency` | string | Да | | Валюта. | | `$.data.items[].service_uuid` | string / null | Да | format: "uuid" | UUID созданной услуги. Отличается от domain UUID. | | `$.data.items[].service_external_id` | string / null | Да | | external_id из payload строки котировки. | | `$.data.invoice` | anyOf | Да | | Счёт или null. | | `$.data.invoice (anyOf 1)` | object | Да | required: ["uuid","number","status","currency","total_minor","paid_minor","payment_collected_by_platform"] | Связанный счёт. Схема: CoreInvoice | | `$.data.invoice (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.invoice (anyOf 1).number` | string | Да | | Номер. | | `$.data.invoice (anyOf 1).status` | string | Да | | Состояние счёта, например issued/paid/cancelled. | | `$.data.invoice (anyOf 1).currency` | string | Да | | Валюта. | | `$.data.invoice (anyOf 1).total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).paid_minor` | integer | Да | format: "int64" | Оплачено. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).payment_collected_by_platform` | boolean | Да | | false при external checkout: оплату клиента принимает реселлер. paid не означает списание с карты платформой. | | `$.data.invoice (anyOf 2)` | null | Да | | | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Заказ хостинга принят (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": "2026-10-05T09:00:00+00:00", "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "hosting", "name": "Hosting Standard", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000042", "service_external_id": "hosting-3043" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "paid_minor": 700000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Не хватает orders.create, domains.register для доменных строк или hosting.order для hosting-строк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Котировка не найдена у текущего ключа/покупателя/реселлера. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Котировка истекла/использована, другая среда, неподдерживаемая операция; test с product; занятый/дублирующий домен; отсутствующие/чужие контакты; несоответствие зоны; повторный external_id; у покупателя нет владельца для заказа. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getOrder.md # Получить заказ `GET /orders/{orderUuid}` **operationId:** `getOrder` Чтение локального состояния без запроса к реестру. В items возвращается service_uuid для дальнейшего GET /services/{serviceUuid}. Отказ реестра может быть отражён асинхронно после HTTP 201. UUID чужого реселлера/среды недоступен. Не трактуйте отсутствие в test как отсутствие в live. **Scopes:** `orders.read` ## Параметры ### orderUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${ORDER_UUID:?Set ORDER_UUID}" curl --request GET "$API_BASE_URL/orders/${ORDER_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/orders/{orderUuid}"; path = path.replaceAll("{orderUuid}", encodeURIComponent(requiredEnv("ORDER_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/orders/{orderUuid}" path = path.replace("{orderUuid}", quote(required_env("ORDER_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/orders/{orderUuid}" value0 := requiredEnv("ORDER_UUID") path = strings.ReplaceAll(path, "{orderUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/orders/{orderUuid}"; path = path.replace("{orderUuid}", URLEncoder.encode(requiredEnv("ORDER_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/orders/{orderUuid}"; path = path.Replace("{orderUuid}", Uri.EscapeDataString(RequiredEnv("ORDER_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreOrderEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","status","currency","total_minor","submitted_at","paid_at","completed_at","failed_at","failure_reason","items","invoice","created_at","updated_at"] | Заказ принят не равнозначно домен зарегистрирован. В external checkout выставляется оплаченный счёт и планируется асинхронное исполнение; дальнейшее состояние отслеживайте через заказ/услугу и webhooks. Схема: CoreOrder | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID заказа внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data.number` | string | Да | | Номер заказа. | | `$.data.status` | string | Да | enum: "draft", "submitted", "payment_authorized", "registration_processing", "registry_pending", "paid", "active", "completed", "registration_failed", "cancelled", "refunded" | Состояние заказа. submitted: выставлен счёт; paid: оплата зафиксирована; registration_processing/registry_pending: работа с реестром; active/completed: исполнен; registration_failed: регистрация неуспешна. Даты и invoice отражают отдельные стадии, HTTP 201 не терминальный статус. | | `$.data.currency` | string | Да | | Валюта. | | `$.data.total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.submitted_at` | string / null | Да | format: "date-time" | Подан. | | `$.data.paid_at` | string / null | Да | format: "date-time" | Оплачен. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Завершён. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Ошибка. | | `$.data.failure_reason` | string / null | Да | | Причина ошибки; не используйте текст как стабильный машинный код. | | `$.data.items` | array | Да | | Строки. | | `$.data.items[]` | object | Да | required: ["uuid","type","name","quantity","unit_price_minor","total_minor","currency","service_uuid","service_external_id"] | Строка заказа. Схема: CoreOrderItem | | `$.data.items[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.items[].type` | string | Да | | Тип услуги, например domain/hosting. | | `$.data.items[].name` | string | Да | | Название. | | `$.data.items[].quantity` | integer | Да | format: "int64" | Количество. | | `$.data.items[].unit_price_minor` | integer | Да | format: "int64" | Цена единицы. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].total_minor` | integer | Да | format: "int64" | Итого. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].currency` | string | Да | | Валюта. | | `$.data.items[].service_uuid` | string / null | Да | format: "uuid" | UUID созданной услуги. Отличается от domain UUID. | | `$.data.items[].service_external_id` | string / null | Да | | external_id из payload строки котировки. | | `$.data.invoice` | anyOf | Да | | Счёт или null. | | `$.data.invoice (anyOf 1)` | object | Да | required: ["uuid","number","status","currency","total_minor","paid_minor","payment_collected_by_platform"] | Связанный счёт. Схема: CoreInvoice | | `$.data.invoice (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.invoice (anyOf 1).number` | string | Да | | Номер. | | `$.data.invoice (anyOf 1).status` | string | Да | | Состояние счёта, например issued/paid/cancelled. | | `$.data.invoice (anyOf 1).currency` | string | Да | | Валюта. | | `$.data.invoice (anyOf 1).total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).paid_minor` | integer | Да | format: "int64" | Оплачено. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).payment_collected_by_platform` | boolean | Да | | false при external checkout: оплату клиента принимает реселлер. paid не означает списание с карты платформой. | | `$.data.invoice (anyOf 2)` | null | Да | | | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Ожидает оплаты (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "submitted", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": null, "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "issued", "currency": "KZT", "total_minor": 700000, "paid_minor": 0, "payment_collected_by_platform": true }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Оплачен, ожидает исполнения (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": "2026-10-05T09:00:00+00:00", "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "paid_minor": 700000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Регистрация завершилась отказом (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "registration_failed", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": "2026-10-05T09:00:00+00:00", "completed_at": null, "failed_at": "2026-10-05T09:03:00+00:00", "failure_reason": "Domain registration was rejected by the registrar.", "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "paid_minor": 700000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:03:00+00:00" } } ``` #### Заказ выполнен (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000030", "external_id": "order-2042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "ORD-20261005090000-DEMO42", "status": "completed", "currency": "KZT", "total_minor": 700000, "submitted_at": "2026-10-05T09:00:00+00:00", "paid_at": "2026-10-05T09:00:00+00:00", "completed_at": "2026-10-05T09:03:00+00:00", "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a0000-0000-7000-8000-000000000031", "type": "domain", "name": "Домен .kz", "quantity": 1, "unit_price_minor": 700000, "total_minor": 700000, "currency": "KZT", "service_uuid": "019a0000-0000-7000-8000-000000000040", "service_external_id": "domain-3042" } ], "invoice": { "uuid": "019a0000-0000-7000-8000-000000000032", "number": "INV-20261005090000-DEMO42", "status": "paid", "currency": "KZT", "total_minor": 700000, "paid_minor": 700000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:03:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listServices.md # Список услуг `GET /services` **operationId:** `listServices` Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. Сортировка id по убыванию. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `services.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ### customer_uuid Расположение: `query`. Необязательный. Точный UUID покупателя. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### external_id Расположение: `query`. Необязательный. Точный ID услуги внешнего биллинга. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### type Расположение: `query`. Необязательный. Тип, например domain или hosting. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### status Расположение: `query`. Необязательный. Точное состояние услуги. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/services?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/services"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/services" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/services" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/services"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/services"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","links","meta"] | Список и ресурсная курсорная пагинация. Схема: CoreServicePage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","external_id","customer_uuid","type","status","name","starts_at","expires_at","auto_renew","domain","hosting","created_at","updated_at"] | Услуга. Список изолирован по реселлеру и fulfillment_environment; доменная связь дополнительно проверяется по среде регистратора. Схема: CoreService | | `$.data[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].external_id` | string / null | Да | | ID услуги внешнего биллинга. | | `$.data[].customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data[].type` | string | Да | | Тип услуги. | | `$.data[].status` | string | Да | | Состояние, например pending/provisioning/active/suspended/failed/cancelled. | | `$.data[].name` | string | Да | | Название. | | `$.data[].starts_at` | string / null | Да | format: "date-time" | Начало периода. | | `$.data[].expires_at` | string / null | Да | format: "date-time" | Конец периода. | | `$.data[].auto_renew` | boolean | Да | | Общий флаг услуги. Это не reseller API согласие на автопродление домена; для него используйте /domains/{uuid}/auto-renew. | | `$.data[].domain` | anyOf | Да | | Краткая карточка домена или null, в том числе до создания доменной записи. | | `$.data[].domain (anyOf 1)` | object | Да | required: ["uuid","external_id","domain_name","status","registration_status","verification_status","registry_status"] | Краткое состояние домена. Полная карточка доступна через /domains/{domainUuid}. Схема: CoreServiceDomain | | `$.data[].domain (anyOf 1).uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data[].domain (anyOf 1).external_id` | string / null | Да | | ID внешнего биллинга. | | `$.data[].domain (anyOf 1).domain_name` | string | Да | | Домен. | | `$.data[].domain (anyOf 1).status` | string | Да | | Статус услуги домена. | | `$.data[].domain (anyOf 1).registration_status` | string | Да | | Состояние регистрации. | | `$.data[].domain (anyOf 1).verification_status` | string | Да | | Состояние проверки. | | `$.data[].domain (anyOf 1).registry_status` | string / null | Да | | Статус реестра. | | `$.data[].domain (anyOf 2)` | null | Да | | | | `$.data[].hosting` | anyOf | Да | | Карточка хостинга или null. | | `$.data[].hosting (anyOf 1)` | object | Да | required: ["uuid","domain_name","status","plan_code","provisioned_at"] | Краткое состояние хостинга. Паролей, panel URL и SSO в этом ресурсе нет. Схема: CoreServiceHosting | | `$.data[].hosting (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].hosting (anyOf 1).domain_name` | string | Да | | Домен хостинга. | | `$.data[].hosting (anyOf 1).status` | string | Да | | Статус хостинга. | | `$.data[].hosting (anyOf 1).plan_code` | string / null | Да | | Код тарифа. | | `$.data[].hosting (anyOf 1).provisioned_at` | string / null | Да | format: "date-time" | Дата фактического создания. | | `$.data[].hosting (anyOf 2)` | null | Да | | | | `$.data[].created_at` | string / null | Да | format: "date-time" | Создана. | | `$.data[].updated_at` | string / null | Да | format: "date-time" | Изменена. | | `$.links` | object | Да | required: ["first","last","prev","next"] | Навигационные ссылки. Курсорная пагинация не вычисляет первую/последнюю страницу. Сохраняйте исходные фильтры при переходе. Схема: CoreResourceLinks | | `$.links.first` | null | Да | | Всегда null. | | `$.links.last` | null | Да | | Всегда null. | | `$.links.prev` | string / null | Да | format: "uri" | Предыдущая страница или null. | | `$.links.next` | string / null | Да | format: "uri" | Следующая страница или null. | | `$.meta` | object | Да | required: ["path","per_page","next_cursor","prev_cursor"] | Пагинация Laravel Resource. Не содержит общего количества записей. Схема: CoreResourceMeta | | `$.meta.path` | string | Да | format: "uri" | URL коллекции. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Размер страницы. | | `$.meta.next_cursor` | string / null | Да | | Непрозрачный курсор следующей страницы. | | `$.meta.prev_cursor` | string / null | Да | | Непрозрачный курсор предыдущей страницы. | #### Доменная и хостинговая услуги (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000040", "external_id": "domain-3042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "type": "domain", "status": "active", "name": "reseller-example.kz", "starts_at": "2026-10-05T09:00:00+00:00", "expires_at": "2027-10-05T09:00:00+00:00", "auto_renew": true, "domain": { "uuid": "019a0000-0000-7000-8000-000000000041", "external_id": "domain-3042", "domain_name": "reseller-example.kz", "status": "active", "registration_status": "registered", "verification_status": "not_required", "registry_status": "ok" }, "hosting": null, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" }, { "uuid": "019a0000-0000-7000-8000-000000000042", "external_id": "hosting-3043", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "type": "hosting", "status": "active", "name": "www.example.com", "starts_at": "2026-10-05T09:00:00+00:00", "expires_at": "2026-11-05T09:00:00+00:00", "auto_renew": true, "domain": null, "hosting": { "uuid": "019a0000-0000-7000-8000-000000000043", "domain_name": "www.example.com", "status": "active", "plan_code": "hosting-standard", "provisioned_at": "2026-10-05T09:00:00+00:00" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.com/api/reseller/v1/services", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.com/api/reseller/v1/services", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getService.md # Получить услугу `GET /services/{serviceUuid}` **operationId:** `getService` Общая карточка услуги с краткими domain/hosting. serviceUuid и domain.uuid/hosting.uuid различаются. Пароли и доступы к панели не выдаются. Общий auto_renew не заменяет отдельное согласие на API-автопродление домена. UUID чужого реселлера/среды недоступен. Не трактуйте отсутствие в test как отсутствие в live. **Scopes:** `services.read` ## Параметры ### serviceUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${SERVICE_UUID:?Set SERVICE_UUID}" curl --request GET "$API_BASE_URL/services/${SERVICE_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/services/{serviceUuid}"; path = path.replaceAll("{serviceUuid}", encodeURIComponent(requiredEnv("SERVICE_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/services/{serviceUuid}" path = path.replace("{serviceUuid}", quote(required_env("SERVICE_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/services/{serviceUuid}" value0 := requiredEnv("SERVICE_UUID") path = strings.ReplaceAll(path, "{serviceUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/services/{serviceUuid}"; path = path.replace("{serviceUuid}", URLEncoder.encode(requiredEnv("SERVICE_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/services/{serviceUuid}"; path = path.Replace("{serviceUuid}", Uri.EscapeDataString(RequiredEnv("SERVICE_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreServiceEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","type","status","name","starts_at","expires_at","auto_renew","domain","hosting","created_at","updated_at"] | Услуга. Список изолирован по реселлеру и fulfillment_environment; доменная связь дополнительно проверяется по среде регистратора. Схема: CoreService | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID услуги внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data.type` | string | Да | | Тип услуги. | | `$.data.status` | string | Да | | Состояние, например pending/provisioning/active/suspended/failed/cancelled. | | `$.data.name` | string | Да | | Название. | | `$.data.starts_at` | string / null | Да | format: "date-time" | Начало периода. | | `$.data.expires_at` | string / null | Да | format: "date-time" | Конец периода. | | `$.data.auto_renew` | boolean | Да | | Общий флаг услуги. Это не reseller API согласие на автопродление домена; для него используйте /domains/{uuid}/auto-renew. | | `$.data.domain` | anyOf | Да | | Краткая карточка домена или null, в том числе до создания доменной записи. | | `$.data.domain (anyOf 1)` | object | Да | required: ["uuid","external_id","domain_name","status","registration_status","verification_status","registry_status"] | Краткое состояние домена. Полная карточка доступна через /domains/{domainUuid}. Схема: CoreServiceDomain | | `$.data.domain (anyOf 1).uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.domain (anyOf 1).external_id` | string / null | Да | | ID внешнего биллинга. | | `$.data.domain (anyOf 1).domain_name` | string | Да | | Домен. | | `$.data.domain (anyOf 1).status` | string | Да | | Статус услуги домена. | | `$.data.domain (anyOf 1).registration_status` | string | Да | | Состояние регистрации. | | `$.data.domain (anyOf 1).verification_status` | string | Да | | Состояние проверки. | | `$.data.domain (anyOf 1).registry_status` | string / null | Да | | Статус реестра. | | `$.data.domain (anyOf 2)` | null | Да | | | | `$.data.hosting` | anyOf | Да | | Карточка хостинга или null. | | `$.data.hosting (anyOf 1)` | object | Да | required: ["uuid","domain_name","status","plan_code","provisioned_at"] | Краткое состояние хостинга. Паролей, panel URL и SSO в этом ресурсе нет. Схема: CoreServiceHosting | | `$.data.hosting (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.hosting (anyOf 1).domain_name` | string | Да | | Домен хостинга. | | `$.data.hosting (anyOf 1).status` | string | Да | | Статус хостинга. | | `$.data.hosting (anyOf 1).plan_code` | string / null | Да | | Код тарифа. | | `$.data.hosting (anyOf 1).provisioned_at` | string / null | Да | format: "date-time" | Дата фактического создания. | | `$.data.hosting (anyOf 2)` | null | Да | | | | `$.data.created_at` | string / null | Да | format: "date-time" | Создана. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменена. | #### Активный домен (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000040", "external_id": "domain-3042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "type": "domain", "status": "active", "name": "reseller-example.kz", "starts_at": "2026-10-05T09:00:00+00:00", "expires_at": "2027-10-05T09:00:00+00:00", "auto_renew": true, "domain": { "uuid": "019a0000-0000-7000-8000-000000000041", "external_id": "domain-3042", "domain_name": "reseller-example.kz", "status": "active", "registration_status": "registered", "verification_status": "not_required", "registry_status": "ok" }, "hosting": null, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Создан заказ, доменный объект ещё отсутствует (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000040", "external_id": "domain-3042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "type": "domain", "status": "pending", "name": "reseller-example.kz", "starts_at": "2026-10-05T09:00:00+00:00", "expires_at": "2027-10-05T09:00:00+00:00", "auto_renew": true, "domain": null, "hosting": null, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Активный хостинг (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000042", "external_id": "hosting-3043", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "type": "hosting", "status": "active", "name": "www.example.com", "starts_at": "2026-10-05T09:00:00+00:00", "expires_at": "2026-11-05T09:00:00+00:00", "auto_renew": true, "domain": null, "hosting": { "uuid": "019a0000-0000-7000-8000-000000000043", "domain_name": "www.example.com", "status": "active", "plan_code": "hosting-standard", "provisioned_at": "2026-10-05T09:00:00+00:00" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listCustomers.md # Список покупателей `GET /customers` **operationId:** `listCustomers` Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. Системные/агрегированные customer metadata.system=true исключены из списка. Сортировка id по возрастанию. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `customers.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ### external_id Расположение: `query`. Необязательный. Точный ID покупателя внешнего биллинга. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/customers?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Список и упрощённая пагинация. Схема: CoreCustomerPage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","external_id","type","status","display_name","email","phone","preferred_currency","locale","timezone","profile","created_at","updated_at"] | Покупатель биллинга, не контакт реестра и не bearer-ключ. Схема: CoreCustomer | | `$.data[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].external_id` | string / null | Да | | Идентификатор клиента во внешнем биллинге. | | `$.data[].type` | string | Да | enum: "individual", "legal" | Тип. | | `$.data[].status` | string | Да | | Состояние клиента. | | `$.data[].display_name` | string | Да | | Отображаемое имя. | | `$.data[].email` | string / null | Да | format: "email" | Email клиента. | | `$.data[].phone` | string / null | Да | | Телефон. | | `$.data[].preferred_currency` | string | Да | | Предпочитаемая валюта. | | `$.data[].locale` | string | Да | | Язык. | | `$.data[].timezone` | string | Да | | Часовой пояс. | | `$.data[].profile` | oneOf | Да | | Для legal возвращается юридический профиль, для individual физический. | | `$.data[].profile (oneOf 1)` | object | Да | required: ["first_name","last_name","middle_name","birth_date","country_code","city","address_line","postal_code"] | Профиль физического лица. Схема: CoreIndividualProfile | | `$.data[].profile (oneOf 1).first_name` | string / null | Да | | Имя. | | `$.data[].profile (oneOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data[].profile (oneOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data[].profile (oneOf 1).birth_date` | string / null | Да | format: "date" | Дата рождения. | | `$.data[].profile (oneOf 1).country_code` | string / null | Да | | Страна, код из двух символов. | | `$.data[].profile (oneOf 1).city` | string / null | Да | | Город. | | `$.data[].profile (oneOf 1).address_line` | string / null | Да | | Адрес. | | `$.data[].profile (oneOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data[].profile (oneOf 2)` | object | Да | required: ["company_name","registration_number","tax_number","legal_address","actual_address","director_full_name"] | Профиль юридического лица. Схема: CoreLegalProfile | | `$.data[].profile (oneOf 2).company_name` | string / null | Да | | Название организации. | | `$.data[].profile (oneOf 2).registration_number` | string / null | Да | | Регистрационный номер. | | `$.data[].profile (oneOf 2).tax_number` | string / null | Да | | Налоговый номер. | | `$.data[].profile (oneOf 2).legal_address` | string / null | Да | | Юридический адрес. | | `$.data[].profile (oneOf 2).actual_address` | string / null | Да | | Фактический адрес. | | `$.data[].profile (oneOf 2).director_full_name` | string / null | Да | | ФИО руководителя. | | `$.data[].created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data[].updated_at` | string / null | Да | format: "date-time" | Изменён. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Упрощённая курсорная пагинация. total, page и prev_cursor отсутствуют. Схема: CoreCursorMeta | | `$.meta.next_cursor` | string / null | Да | | Передать как cursor следующего запроса. null означает конец. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Фактический размер страницы. | #### Физическое и юридическое лицо (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "customer-1042", "type": "individual", "status": "active", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" }, { "uuid": "019a0000-0000-7000-8000-000000000011", "external_id": "company-1043", "type": "legal", "status": "active", "display_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "company_name": "ТОО \"Example\"", "registration_number": "EXAMPLE-2026", "tax_number": null, "legal_address": "Алматы, ул. Примерная, 10", "actual_address": "Алматы, ул. Примерная, 10", "director_full_name": "Иван Петров" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createCustomer.md # Создать покупателя `POST /customers` **operationId:** `createCustomer` Только customer_mode=managed. Создаётся покупатель и профиль; live может связать его с существующей пользовательской записью по email. test использует изолированную неактивную пользовательскую запись. Пароль и токен клиентского входа не выдаются. Это не создаёт контакт реестра: отдельно POST /contacts. status из запроса не меняет начальный active. **Scopes:** `customers.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["type","display_name","email"] | Создание доступно только customer_mode=managed. Поля профиля передаются на верхнем уровне, не в profile. API не возвращает пароль. test создаёт изолированного неактивного пользователя, не использует live-учётную запись по email. Схема: CoreCreateCustomerRequest | | `$.external_id` | string / null | Нет | maxLength: 255 | Уникален внутри реселлера и среды test/live. | | `$.type` | string | Да | enum: "individual", "legal" | Тип клиента. | | `$.status` | string | Нет | enum: "active", "blocked" | При создании принимается, но итоговый статус всегда active. При PATCH изменяется. | | `$.display_name` | string | Да | maxLength: 255 | Отображаемое имя. | | `$.email` | string | Да | format: "email"; maxLength: 255 | Email; при создании приводится к нижнему регистру. | | `$.phone` | string / null | Нет | maxLength: 50 | Телефон; null при PATCH не очищает существующее значение. | | `$.preferred_currency` | string | Нет | minLength: 3; maxLength: 3 | Валюта, при создании верхний регистр. | | `$.locale` | string | Нет | enum: "ru", "kk", "en" | Язык. | | `$.timezone` | string | Нет | | Допустимый часовой пояс IANA. | | `$.company_name` | string / null | Нет | maxLength: 255 | Для legal; при отсутствии используется display_name. При PATCH отсутствие может заменить название текущим display_name. | | `$.registration_number` | string / null | Нет | maxLength: 100 | Для legal. | | `$.tax_number` | string / null | Нет | maxLength: 100 | Для legal. | | `$.legal_address` | string / null | Нет | maxLength: 500 | Для legal. | | `$.actual_address` | string / null | Нет | maxLength: 500 | Для legal. | | `$.director_full_name` | string / null | Нет | maxLength: 255 | Для legal. | | `$.first_name` | string / null | Нет | maxLength: 100 | Для individual; при создании по умолчанию display_name. | | `$.last_name` | string / null | Нет | maxLength: 100 | Для individual; при создании по умолчанию display_name. | | `$.middle_name` | string / null | Нет | maxLength: 100 | Для individual. | | `$.birth_date` | string / null | Нет | format: "date" | Дата строго раньше сегодняшней. | | `$.country_code` | string / null | Нет | minLength: 2; maxLength: 2 | Код страны; по умолчанию KZ. | | `$.city` | string / null | Нет | maxLength: 120 | Город. | | `$.address_line` | string / null | Нет | maxLength: 500 | Адрес. | | `$.postal_code` | string / null | Нет | maxLength: 30 | Индекс. | #### Физическое лицо ```json { "external_id": "customer-1042", "type": "individual", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/customers" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "external_id": "customer-1042", "type": "individual", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers"; const url = baseUrl + path; const body = `{ "external_id": "customer-1042", "type": "individual", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"external_id\": \"customer-1042\",\n \"type\": \"individual\",\n \"display_name\": \"Иван Петров\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"birth_date\": \"1990-01-01\",\n \"country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"external_id\": \"customer-1042\",\n \"type\": \"individual\",\n \"display_name\": \"Иван Петров\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"birth_date\": \"1990-01-01\",\n \"country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers"; URI uri = URI.create(baseUrl + path); String body = "{\n \"external_id\": \"customer-1042\",\n \"type\": \"individual\",\n \"display_name\": \"Иван Петров\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"birth_date\": \"1990-01-01\",\n \"country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"external_id\": \"customer-1042\",\n \"type\": \"individual\",\n \"display_name\": \"Иван Петров\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"birth_date\": \"1990-01-01\",\n \"country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Юридическое лицо ```json { "external_id": "company-1043", "type": "legal", "display_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "company_name": "ТОО \"Example\"", "registration_number": "EXAMPLE-2026", "tax_number": null, "legal_address": "Алматы, ул. Примерная, 10", "actual_address": "Алматы, ул. Примерная, 10", "director_full_name": "Иван Петров" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/customers" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "external_id": "company-1043", "type": "legal", "display_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "company_name": "ТОО \"Example\"", "registration_number": "EXAMPLE-2026", "tax_number": null, "legal_address": "Алматы, ул. Примерная, 10", "actual_address": "Алматы, ул. Примерная, 10", "director_full_name": "Иван Петров" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers"; const url = baseUrl + path; const body = `{ "external_id": "company-1043", "type": "legal", "display_name": "ТОО \\"Example\\"", "email": "admin@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "company_name": "ТОО \\"Example\\"", "registration_number": "EXAMPLE-2026", "tax_number": null, "legal_address": "Алматы, ул. Примерная, 10", "actual_address": "Алматы, ул. Примерная, 10", "director_full_name": "Иван Петров" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"external_id\": \"company-1043\",\n \"type\": \"legal\",\n \"display_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"registration_number\": \"EXAMPLE-2026\",\n \"tax_number\": null,\n \"legal_address\": \"Алматы, ул. Примерная, 10\",\n \"actual_address\": \"Алматы, ул. Примерная, 10\",\n \"director_full_name\": \"Иван Петров\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"external_id\": \"company-1043\",\n \"type\": \"legal\",\n \"display_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"registration_number\": \"EXAMPLE-2026\",\n \"tax_number\": null,\n \"legal_address\": \"Алматы, ул. Примерная, 10\",\n \"actual_address\": \"Алматы, ул. Примерная, 10\",\n \"director_full_name\": \"Иван Петров\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers"; URI uri = URI.create(baseUrl + path); String body = "{\n \"external_id\": \"company-1043\",\n \"type\": \"legal\",\n \"display_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"registration_number\": \"EXAMPLE-2026\",\n \"tax_number\": null,\n \"legal_address\": \"Алматы, ул. Примерная, 10\",\n \"actual_address\": \"Алматы, ул. Примерная, 10\",\n \"director_full_name\": \"Иван Петров\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"external_id\": \"company-1043\",\n \"type\": \"legal\",\n \"display_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"preferred_currency\": \"KZT\",\n \"locale\": \"ru\",\n \"timezone\": \"Asia/Almaty\",\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"registration_number\": \"EXAMPLE-2026\",\n \"tax_number\": null,\n \"legal_address\": \"Алматы, ул. Примерная, 10\",\n \"actual_address\": \"Алматы, ул. Примерная, 10\",\n \"director_full_name\": \"Иван Петров\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Режим aggregate не разрешает создание покупателей либо нет customers.write. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Неверный email/тип/дата/язык/часовой пояс; повторный external_id внутри реселлера+среды. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreCustomerEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","type","status","display_name","email","phone","preferred_currency","locale","timezone","profile","created_at","updated_at"] | Покупатель биллинга, не контакт реестра и не bearer-ключ. Схема: CoreCustomer | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | Идентификатор клиента во внешнем биллинге. | | `$.data.type` | string | Да | enum: "individual", "legal" | Тип. | | `$.data.status` | string | Да | | Состояние клиента. | | `$.data.display_name` | string | Да | | Отображаемое имя. | | `$.data.email` | string / null | Да | format: "email" | Email клиента. | | `$.data.phone` | string / null | Да | | Телефон. | | `$.data.preferred_currency` | string | Да | | Предпочитаемая валюта. | | `$.data.locale` | string | Да | | Язык. | | `$.data.timezone` | string | Да | | Часовой пояс. | | `$.data.profile` | oneOf | Да | | Для legal возвращается юридический профиль, для individual физический. | | `$.data.profile (oneOf 1)` | object | Да | required: ["first_name","last_name","middle_name","birth_date","country_code","city","address_line","postal_code"] | Профиль физического лица. Схема: CoreIndividualProfile | | `$.data.profile (oneOf 1).first_name` | string / null | Да | | Имя. | | `$.data.profile (oneOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.profile (oneOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.profile (oneOf 1).birth_date` | string / null | Да | format: "date" | Дата рождения. | | `$.data.profile (oneOf 1).country_code` | string / null | Да | | Страна, код из двух символов. | | `$.data.profile (oneOf 1).city` | string / null | Да | | Город. | | `$.data.profile (oneOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.profile (oneOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.profile (oneOf 2)` | object | Да | required: ["company_name","registration_number","tax_number","legal_address","actual_address","director_full_name"] | Профиль юридического лица. Схема: CoreLegalProfile | | `$.data.profile (oneOf 2).company_name` | string / null | Да | | Название организации. | | `$.data.profile (oneOf 2).registration_number` | string / null | Да | | Регистрационный номер. | | `$.data.profile (oneOf 2).tax_number` | string / null | Да | | Налоговый номер. | | `$.data.profile (oneOf 2).legal_address` | string / null | Да | | Юридический адрес. | | `$.data.profile (oneOf 2).actual_address` | string / null | Да | | Фактический адрес. | | `$.data.profile (oneOf 2).director_full_name` | string / null | Да | | ФИО руководителя. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Физическое лицо (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "customer-1042", "type": "individual", "status": "active", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Юридическое лицо (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000011", "external_id": "company-1043", "type": "legal", "status": "active", "display_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "company_name": "ТОО \"Example\"", "registration_number": "EXAMPLE-2026", "tax_number": null, "legal_address": "Алматы, ул. Примерная, 10", "actual_address": "Алматы, ул. Примерная, 10", "director_full_name": "Иван Петров" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Режим aggregate не разрешает создание покупателей либо нет customers.write. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Неверный email/тип/дата/язык/часовой пояс; повторный external_id внутри реселлера+среды. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getCustomer.md # Получить покупателя `GET /customers/{customerUuid}` **operationId:** `getCustomer` UUID чужого реселлера/среды недоступен. Не трактуйте отсутствие в test как отсутствие в live. Системные покупатели исключены. **Scopes:** `customers.read` ## Параметры ### customerUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CUSTOMER_UUID:?Set CUSTOMER_UUID}" curl --request GET "$API_BASE_URL/customers/${CUSTOMER_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers/{customerUuid}"; path = path.replaceAll("{customerUuid}", encodeURIComponent(requiredEnv("CUSTOMER_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers/{customerUuid}" path = path.replace("{customerUuid}", quote(required_env("CUSTOMER_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers/{customerUuid}" value0 := requiredEnv("CUSTOMER_UUID") path = strings.ReplaceAll(path, "{customerUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers/{customerUuid}"; path = path.replace("{customerUuid}", URLEncoder.encode(requiredEnv("CUSTOMER_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers/{customerUuid}"; path = path.Replace("{customerUuid}", Uri.EscapeDataString(RequiredEnv("CUSTOMER_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreCustomerEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","type","status","display_name","email","phone","preferred_currency","locale","timezone","profile","created_at","updated_at"] | Покупатель биллинга, не контакт реестра и не bearer-ключ. Схема: CoreCustomer | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | Идентификатор клиента во внешнем биллинге. | | `$.data.type` | string | Да | enum: "individual", "legal" | Тип. | | `$.data.status` | string | Да | | Состояние клиента. | | `$.data.display_name` | string | Да | | Отображаемое имя. | | `$.data.email` | string / null | Да | format: "email" | Email клиента. | | `$.data.phone` | string / null | Да | | Телефон. | | `$.data.preferred_currency` | string | Да | | Предпочитаемая валюта. | | `$.data.locale` | string | Да | | Язык. | | `$.data.timezone` | string | Да | | Часовой пояс. | | `$.data.profile` | oneOf | Да | | Для legal возвращается юридический профиль, для individual физический. | | `$.data.profile (oneOf 1)` | object | Да | required: ["first_name","last_name","middle_name","birth_date","country_code","city","address_line","postal_code"] | Профиль физического лица. Схема: CoreIndividualProfile | | `$.data.profile (oneOf 1).first_name` | string / null | Да | | Имя. | | `$.data.profile (oneOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.profile (oneOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.profile (oneOf 1).birth_date` | string / null | Да | format: "date" | Дата рождения. | | `$.data.profile (oneOf 1).country_code` | string / null | Да | | Страна, код из двух символов. | | `$.data.profile (oneOf 1).city` | string / null | Да | | Город. | | `$.data.profile (oneOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.profile (oneOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.profile (oneOf 2)` | object | Да | required: ["company_name","registration_number","tax_number","legal_address","actual_address","director_full_name"] | Профиль юридического лица. Схема: CoreLegalProfile | | `$.data.profile (oneOf 2).company_name` | string / null | Да | | Название организации. | | `$.data.profile (oneOf 2).registration_number` | string / null | Да | | Регистрационный номер. | | `$.data.profile (oneOf 2).tax_number` | string / null | Да | | Налоговый номер. | | `$.data.profile (oneOf 2).legal_address` | string / null | Да | | Юридический адрес. | | `$.data.profile (oneOf 2).actual_address` | string / null | Да | | Фактический адрес. | | `$.data.profile (oneOf 2).director_full_name` | string / null | Да | | ФИО руководителя. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Физическое лицо (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "customer-1042", "type": "individual", "status": "active", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Организация (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000011", "external_id": "company-1043", "type": "legal", "status": "active", "display_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "company_name": "ТОО \"Example\"", "registration_number": "EXAMPLE-2026", "tax_number": null, "legal_address": "Алматы, ул. Примерная, 10", "actual_address": "Алматы, ул. Примерная, 10", "director_full_name": "Иван Петров" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/updateCustomer.md # Обновить покупателя `PATCH /customers/{customerUuid}` **operationId:** `updateCustomer` Частичный PATCH верхнего уровня. type/email/preferred_currency валидируются, но не изменяются текущим контроллером. null не очищает телефон, external_id или поля профиля. company_name у legal при отсутствии заменяется display_name; передавайте прежнее company_name, если его нужно сохранить. Изменение status не является документированной командой приостановки всех услуг. **Scopes:** `customers.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ### customerUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: [] | Частичное обновление. type, email, preferred_currency проходят валидацию, но текущий контроллер НЕ изменяет их. Не используйте PATCH для смены этих полей. null для полей профиля сохраняет прежнее значение. Не передавайте вложенный profile. Схема: CoreUpdateCustomerRequest | | `$.external_id` | string / null | Нет | maxLength: 255 | Уникален внутри реселлера и среды test/live. | | `$.type` | string | Нет | enum: "individual", "legal" | При PATCH принимается, но не изменяется. | | `$.status` | string | Нет | enum: "active", "blocked" | При создании принимается, но итоговый статус всегда active. При PATCH изменяется. | | `$.display_name` | string | Нет | maxLength: 255 | Отображаемое имя. | | `$.email` | string | Нет | format: "email"; maxLength: 255 | При PATCH принимается, но не изменяется. | | `$.phone` | string / null | Нет | maxLength: 50 | Телефон; null при PATCH не очищает существующее значение. | | `$.preferred_currency` | string | Нет | minLength: 3; maxLength: 3 | При PATCH принимается, но не изменяется. | | `$.locale` | string | Нет | enum: "ru", "kk", "en" | Язык. | | `$.timezone` | string | Нет | | Допустимый часовой пояс IANA. | | `$.company_name` | string / null | Нет | maxLength: 255 | Для legal; при отсутствии используется display_name. При PATCH отсутствие может заменить название текущим display_name. | | `$.registration_number` | string / null | Нет | maxLength: 100 | Для legal. | | `$.tax_number` | string / null | Нет | maxLength: 100 | Для legal. | | `$.legal_address` | string / null | Нет | maxLength: 500 | Для legal. | | `$.actual_address` | string / null | Нет | maxLength: 500 | Для legal. | | `$.director_full_name` | string / null | Нет | maxLength: 255 | Для legal. | | `$.first_name` | string / null | Нет | maxLength: 100 | Для individual; при создании по умолчанию display_name. | | `$.last_name` | string / null | Нет | maxLength: 100 | Для individual; при создании по умолчанию display_name. | | `$.middle_name` | string / null | Нет | maxLength: 100 | Для individual. | | `$.birth_date` | string / null | Нет | format: "date" | Дата строго раньше сегодняшней. | | `$.country_code` | string / null | Нет | minLength: 2; maxLength: 2 | Код страны; по умолчанию KZ. | | `$.city` | string / null | Нет | maxLength: 120 | Город. | | `$.address_line` | string / null | Нет | maxLength: 500 | Адрес. | | `$.postal_code` | string / null | Нет | maxLength: 30 | Индекс. | #### Изменить телефон физлица ```json { "phone": "+77010000001" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CUSTOMER_UUID:?Set CUSTOMER_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/customers/${CUSTOMER_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "phone": "+77010000001" }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers/{customerUuid}"; path = path.replaceAll("{customerUuid}", encodeURIComponent(requiredEnv("CUSTOMER_UUID"))); const url = baseUrl + path; const body = `{ "phone": "+77010000001" }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers/{customerUuid}" path = path.replace("{customerUuid}", quote(required_env("CUSTOMER_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"phone\": \"+77010000001\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers/{customerUuid}" value0 := requiredEnv("CUSTOMER_UUID") path = strings.ReplaceAll(path, "{customerUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"phone\": \"+77010000001\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers/{customerUuid}"; path = path.replace("{customerUuid}", URLEncoder.encode(requiredEnv("CUSTOMER_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"phone\": \"+77010000001\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers/{customerUuid}"; path = path.Replace("{customerUuid}", Uri.EscapeDataString(RequiredEnv("CUSTOMER_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"phone\": \"+77010000001\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Изменить адрес с сохранением названия ```json { "company_name": "ТОО \"Example\"", "legal_address": "Алматы, ул. Примерная, 20" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CUSTOMER_UUID:?Set CUSTOMER_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/customers/${CUSTOMER_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "company_name": "ТОО \"Example\"", "legal_address": "Алматы, ул. Примерная, 20" }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers/{customerUuid}"; path = path.replaceAll("{customerUuid}", encodeURIComponent(requiredEnv("CUSTOMER_UUID"))); const url = baseUrl + path; const body = `{ "company_name": "ТОО \\"Example\\"", "legal_address": "Алматы, ул. Примерная, 20" }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers/{customerUuid}" path = path.replace("{customerUuid}", quote(required_env("CUSTOMER_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"legal_address\": \"Алматы, ул. Примерная, 20\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers/{customerUuid}" value0 := requiredEnv("CUSTOMER_UUID") path = strings.ReplaceAll(path, "{customerUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"legal_address\": \"Алматы, ул. Примерная, 20\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers/{customerUuid}"; path = path.replace("{customerUuid}", URLEncoder.encode(requiredEnv("CUSTOMER_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"legal_address\": \"Алматы, ул. Примерная, 20\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers/{customerUuid}"; path = path.Replace("{customerUuid}", Uri.EscapeDataString(RequiredEnv("CUSTOMER_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"company_name\": \"ТОО \\\"Example\\\"\",\n \"legal_address\": \"Алматы, ул. Примерная, 20\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Изменить состояние покупателя ```json { "status": "blocked" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CUSTOMER_UUID:?Set CUSTOMER_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/customers/${CUSTOMER_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "status": "blocked" }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/customers/{customerUuid}"; path = path.replaceAll("{customerUuid}", encodeURIComponent(requiredEnv("CUSTOMER_UUID"))); const url = baseUrl + path; const body = `{ "status": "blocked" }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/customers/{customerUuid}" path = path.replace("{customerUuid}", quote(required_env("CUSTOMER_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"status\": \"blocked\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/customers/{customerUuid}" value0 := requiredEnv("CUSTOMER_UUID") path = strings.ReplaceAll(path, "{customerUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"status\": \"blocked\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/customers/{customerUuid}"; path = path.replace("{customerUuid}", URLEncoder.encode(requiredEnv("CUSTOMER_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"status\": \"blocked\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/customers/{customerUuid}"; path = path.Replace("{customerUuid}", Uri.EscapeDataString(RequiredEnv("CUSTOMER_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"status\": \"blocked\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Неверные значения или занятый external_id. Поля профиля должны находиться на верхнем уровне. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreCustomerEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","type","status","display_name","email","phone","preferred_currency","locale","timezone","profile","created_at","updated_at"] | Покупатель биллинга, не контакт реестра и не bearer-ключ. Схема: CoreCustomer | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | Идентификатор клиента во внешнем биллинге. | | `$.data.type` | string | Да | enum: "individual", "legal" | Тип. | | `$.data.status` | string | Да | | Состояние клиента. | | `$.data.display_name` | string | Да | | Отображаемое имя. | | `$.data.email` | string / null | Да | format: "email" | Email клиента. | | `$.data.phone` | string / null | Да | | Телефон. | | `$.data.preferred_currency` | string | Да | | Предпочитаемая валюта. | | `$.data.locale` | string | Да | | Язык. | | `$.data.timezone` | string | Да | | Часовой пояс. | | `$.data.profile` | oneOf | Да | | Для legal возвращается юридический профиль, для individual физический. | | `$.data.profile (oneOf 1)` | object | Да | required: ["first_name","last_name","middle_name","birth_date","country_code","city","address_line","postal_code"] | Профиль физического лица. Схема: CoreIndividualProfile | | `$.data.profile (oneOf 1).first_name` | string / null | Да | | Имя. | | `$.data.profile (oneOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.profile (oneOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.profile (oneOf 1).birth_date` | string / null | Да | format: "date" | Дата рождения. | | `$.data.profile (oneOf 1).country_code` | string / null | Да | | Страна, код из двух символов. | | `$.data.profile (oneOf 1).city` | string / null | Да | | Город. | | `$.data.profile (oneOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.profile (oneOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.profile (oneOf 2)` | object | Да | required: ["company_name","registration_number","tax_number","legal_address","actual_address","director_full_name"] | Профиль юридического лица. Схема: CoreLegalProfile | | `$.data.profile (oneOf 2).company_name` | string / null | Да | | Название организации. | | `$.data.profile (oneOf 2).registration_number` | string / null | Да | | Регистрационный номер. | | `$.data.profile (oneOf 2).tax_number` | string / null | Да | | Налоговый номер. | | `$.data.profile (oneOf 2).legal_address` | string / null | Да | | Юридический адрес. | | `$.data.profile (oneOf 2).actual_address` | string / null | Да | | Фактический адрес. | | `$.data.profile (oneOf 2).director_full_name` | string / null | Да | | ФИО руководителя. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Изменён телефон (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "customer-1042", "type": "individual", "status": "active", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000001", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` #### Покупатель отмечен blocked (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "customer-1042", "type": "individual", "status": "blocked", "display_name": "Иван Петров", "email": "ivan@example.com", "phone": "+77010000000", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty", "profile": { "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "birth_date": "1990-01-01", "country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000" }, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Неверные значения или занятый external_id. Поля профиля должны находиться на верхнем уровне. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listContacts.md # Контакты покупателя `GET /contacts` **operationId:** `listContacts` Возвращает локальные контакты выбранного покупателя, сортировка id по возрастанию. В managed customer_uuid обязателен, отсутствие даёт 422. Фильтр external_id не реализован. Значение документа не возвращается. Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `contacts.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ### customer_uuid Расположение: `query`. Необязательный. Обязателен в managed; в aggregate отсутствие выбирает агрегированного покупателя. Неверный/чужой customer_uuid даёт 422, не пустой список. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string / null | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/contacts?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | customer_uuid отсутствует в managed, имеет неверный формат либо не найден у реселлера в данной среде. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Список и упрощённая пагинация. Схема: CoreContactPage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at","external_id","customer_uuid"] | Локальный доменный контакт. external_id_value никогда не возвращается. Изменение этой карточки само по себе не выполняет EPP contact update. Схема: CoreContact | | `$.data[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].type` | string | Да | enum: "person", "organization" | Тип. | | `$.data[].status` | string | Да | | Статус, при создании active. | | `$.data[].first_name` | string / null | Да | | Имя. | | `$.data[].last_name` | string / null | Да | | Фамилия. | | `$.data[].middle_name` | string / null | Да | | Отчество. | | `$.data[].organization_name` | string / null | Да | | Организация. | | `$.data[].email` | string | Да | format: "email" | Email. | | `$.data[].phone` | string | Да | | Телефон. | | `$.data[].country_code` | string | Да | | Страна адреса. | | `$.data[].residence_country_code` | string / null | Да | | Страна резидентства. | | `$.data[].city` | string | Да | | Город. | | `$.data[].region` | string / null | Да | | Регион. | | `$.data[].address_line` | string | Да | | Адрес. | | `$.data[].postal_code` | string / null | Да | | Почтовый индекс. | | `$.data[].external_id_type` | string / null | Да | | Тип документа, не идентификатор внешнего биллинга. | | `$.data[].verification_status` | string | Да | | Статус верификации; создание обычно not_required. | | `$.data[].is_locked_as_registrant` | boolean | Да | | Признак блокировки контакта как владельца. | | `$.data[].verified_at` | string / null | Да | format: "date-time" | Время подтверждения. | | `$.data[].external_id` | string / null | Да | | Идентификатор контакта во внешнем биллинге. | | `$.data[].customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Упрощённая курсорная пагинация. total, page и prev_cursor отсутствуют. Схема: CoreCursorMeta | | `$.meta.next_cursor` | string / null | Да | | Передать как cursor следующего запроса. null означает конец. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Фактический размер страницы. | #### Контакты одного покупателя (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000012", "type": "person", "status": "active", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 customer_uuid отсутствует в managed, имеет неверный формат либо не найден у реселлера в данной среде. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createContact.md # Создать контакт для домена `POST /contacts` **operationId:** `createContact` Создаёт локальную карточку. Контакт реестра создаётся/связывается при доменных операциях, не этой командой. Для person обязательно отчество. residence_country_code и postal_code обязательны несмотря на необязательность в старых примерах. Правила документа зависят от страны резидентства, не от среды ключа. **Scopes:** `contacts.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["type","email","phone","country_code","residence_country_code","city","address_line","postal_code"] | POST и PATCH используют одинаковую полную валидацию: PATCH не частичный. Неуказанные необязательные текстовые поля обнуляются; external_id сохраняется при отсутствии. Документ при отсутствии обоих полей сохраняется. customer_uuid обязателен в managed. Схема: CoreUpsertContactRequest | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Обязателен в managed. При aggregate можно опустить для агрегированного покупателя. | | `$.external_id` | string / null | Нет | maxLength: 255 | Идентификатор во внешнем биллинге; уникален для реселлера+среды. | | `$.type` | string | Да | enum: "person", "organization" | Тип контакта. | | `$.first_name` | string / null | Нет | maxLength: 255 | Обязателен для person. Unicode-буквы/диакритика, одиночные пробелы, дефис или апостроф между словами. | | `$.last_name` | string / null | Нет | maxLength: 255 | Обязателен для person; те же правила имени. | | `$.middle_name` | string / null | Нет | maxLength: 255 | В текущем API отчество ОБЯЗАТЕЛЬНО для person, не только имя и фамилия. | | `$.organization_name` | string / null | Нет | maxLength: 500 | Обязательно для organization. | | `$.email` | string | Да | format: "email"; maxLength: 255 | Email. | | `$.phone` | string | Да | maxLength: 50 | Телефон. Контроллер ограничивает длину, не проверяет E.164; используйте международный формат. | | `$.country_code` | string | Да | minLength: 2; maxLength: 2 | Страна адреса, верхний регистр сохраняется сервером. | | `$.residence_country_code` | string | Да | minLength: 2; maxLength: 2 | Обязательная страна резидентства; определяет правила документа. | | `$.city` | string | Да | maxLength: 255 | Город. | | `$.region` | string / null | Нет | maxLength: 255 | Регион. | | `$.address_line` | string | Да | maxLength: 1000 | Адрес. | | `$.postal_code` | string | Да | maxLength: 30 | Обязательный индекс. | | `$.external_id_type` | string / null | Нет | maxLength: 40 | Обязателен с external_id_value. KZ: BIN/IIN (12 цифр и контрольная сумма); RU: INN (10/12 цифр); US: EIN (NN-NNNNNNN), SSN (NNN-NN-NNNN); CN: USCC (18 символов); DE: VAT (DE+9 цифр); UZ: STIR (9), PINFL (14); GB: CRN (8), VAT (GB+9); TR: VKN (10), TCKN (11); UA: EDRPOU (8); AE: TRN (100+12). Для остальных стран PASSPORT/TAX_ID (непустое значение). USCC допускает [0-9A-HJ-NP-RT-UW-Y]{18}. Форматы регистрозависимы для значения документа, тип/страна нормализуются к верхнему регистру. | | `$.external_id_value` | string / null | Нет | maxLength: 100; Только запись | Обязателен с external_id_type; хранится, но не возвращается. Для KZ проверяется контрольная сумма; тестовые исключения зависят от конфигурации, test-ключ не отключает проверку. | | `$ (allOf 1)` | Условная схема | Да | | | | `$ (allOf 1) (if)` | object | Да | | | | `$ (allOf 1) (if).type` | Условная схема | Нет | const: "person" | | | `$ (allOf 1) (then)` | object | Да | required: ["first_name","last_name","middle_name"] | | | `$ (allOf 1) (then).first_name` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).last_name` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).middle_name` | string | Да | minLength: 1 | | | `$ (allOf 2)` | Условная схема | Да | | | | `$ (allOf 2) (if)` | object | Да | | | | `$ (allOf 2) (if).type` | Условная схема | Нет | const: "organization" | | | `$ (allOf 2) (then)` | object | Да | required: ["organization_name"] | | | `$ (allOf 2) (then).organization_name` | string | Да | minLength: 1 | | #### Полный контакт физического лица ```json { "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path; const body = `{ "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path); String body = "{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Контакт с ИИН; синтетический пример с корректной контрольной суммой, замените данными владельца ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "contact-1044", "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": "IIN", "external_id_value": "900101300116" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "contact-1044", "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": "IIN", "external_id_value": "900101300116" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "contact-1044", "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": "IIN", "external_id_value": "900101300116" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"contact-1044\",\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id_type\": \"IIN\",\n \"external_id_value\": \"900101300116\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Полный контакт организации ```json { "type": "organization", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "organization", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts"; const url = baseUrl + path; const body = `{ "type": "organization", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \\"Example\\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts"; URI uri = URI.create(baseUrl + path); String body = "{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"type\": \"organization\",\n \"first_name\": null,\n \"last_name\": null,\n \"middle_name\": null,\n \"organization_name\": \"ТОО \\\"Example\\\"\",\n \"email\": \"admin@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 10\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1043\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000011\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Отсутствуют обязательные поля (включая middle_name для person), неверный документ/контрольная сумма, customer_uuid не разрешён, external_id уже занят. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreContactEnvelope | | `$.data` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at","external_id","customer_uuid"] | Локальный доменный контакт. external_id_value никогда не возвращается. Изменение этой карточки само по себе не выполняет EPP contact update. Схема: CoreContact | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.type` | string | Да | enum: "person", "organization" | Тип. | | `$.data.status` | string | Да | | Статус, при создании active. | | `$.data.first_name` | string / null | Да | | Имя. | | `$.data.last_name` | string / null | Да | | Фамилия. | | `$.data.middle_name` | string / null | Да | | Отчество. | | `$.data.organization_name` | string / null | Да | | Организация. | | `$.data.email` | string | Да | format: "email" | Email. | | `$.data.phone` | string | Да | | Телефон. | | `$.data.country_code` | string | Да | | Страна адреса. | | `$.data.residence_country_code` | string / null | Да | | Страна резидентства. | | `$.data.city` | string | Да | | Город. | | `$.data.region` | string / null | Да | | Регион. | | `$.data.address_line` | string | Да | | Адрес. | | `$.data.postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.external_id_type` | string / null | Да | | Тип документа, не идентификатор внешнего биллинга. | | `$.data.verification_status` | string | Да | | Статус верификации; создание обычно not_required. | | `$.data.is_locked_as_registrant` | boolean | Да | | Признак блокировки контакта как владельца. | | `$.data.verified_at` | string / null | Да | format: "date-time" | Время подтверждения. | | `$.data.external_id` | string / null | Да | | Идентификатор контакта во внешнем биллинге. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | #### Физическое лицо без документа; для регистрации документ может понадобиться (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000012", "type": "person", "status": "active", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } } ``` #### Организация (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000013", "type": "organization", "status": "active", "first_name": null, "last_name": null, "middle_name": null, "organization_name": "ТОО \"Example\"", "email": "admin@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1043", "customer_uuid": "019a0000-0000-7000-8000-000000000011" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Отсутствуют обязательные поля (включая middle_name для person), неверный документ/контрольная сумма, customer_uuid не разрешён, external_id уже занят. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getContact.md # Получить контакт `GET /contacts/{contactUuid}` **operationId:** `getContact` В managed customer_uuid нужен даже при известном contactUuid. external_id_value не возвращается. UUID чужого реселлера/среды недоступен. Не трактуйте отсутствие в test как отсутствие в live. **Scopes:** `contacts.read` ## Параметры ### contactUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### customer_uuid Расположение: `query`. Необязательный. Обязателен в managed; в aggregate отсутствие выбирает агрегированного покупателя. Неверный/чужой customer_uuid даёт 422, не пустой список. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string / null | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CONTACT_UUID:?Set CONTACT_UUID}" curl --request GET "$API_BASE_URL/contacts/${CONTACT_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts/{contactUuid}"; path = path.replaceAll("{contactUuid}", encodeURIComponent(requiredEnv("CONTACT_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts/{contactUuid}" path = path.replace("{contactUuid}", quote(required_env("CONTACT_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts/{contactUuid}" value0 := requiredEnv("CONTACT_UUID") path = strings.ReplaceAll(path, "{contactUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts/{contactUuid}"; path = path.replace("{contactUuid}", URLEncoder.encode(requiredEnv("CONTACT_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts/{contactUuid}"; path = path.Replace("{contactUuid}", Uri.EscapeDataString(RequiredEnv("CONTACT_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Контакт не принадлежит выбранному покупателю или среде. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Неверный/отсутствующий customer_uuid согласно режиму. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreContactEnvelope | | `$.data` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at","external_id","customer_uuid"] | Локальный доменный контакт. external_id_value никогда не возвращается. Изменение этой карточки само по себе не выполняет EPP contact update. Схема: CoreContact | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.type` | string | Да | enum: "person", "organization" | Тип. | | `$.data.status` | string | Да | | Статус, при создании active. | | `$.data.first_name` | string / null | Да | | Имя. | | `$.data.last_name` | string / null | Да | | Фамилия. | | `$.data.middle_name` | string / null | Да | | Отчество. | | `$.data.organization_name` | string / null | Да | | Организация. | | `$.data.email` | string | Да | format: "email" | Email. | | `$.data.phone` | string | Да | | Телефон. | | `$.data.country_code` | string | Да | | Страна адреса. | | `$.data.residence_country_code` | string / null | Да | | Страна резидентства. | | `$.data.city` | string | Да | | Город. | | `$.data.region` | string / null | Да | | Регион. | | `$.data.address_line` | string | Да | | Адрес. | | `$.data.postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.external_id_type` | string / null | Да | | Тип документа, не идентификатор внешнего биллинга. | | `$.data.verification_status` | string | Да | | Статус верификации; создание обычно not_required. | | `$.data.is_locked_as_registrant` | boolean | Да | | Признак блокировки контакта как владельца. | | `$.data.verified_at` | string / null | Да | format: "date-time" | Время подтверждения. | | `$.data.external_id` | string / null | Да | | Идентификатор контакта во внешнем биллинге. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | #### Локальная карточка (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000012", "type": "person", "status": "active", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 10", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Контакт не принадлежит выбранному покупателю или среде. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Неверный/отсутствующий customer_uuid согласно режиму. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/updateContact.md # Заменить данные контакта `PATCH /contacts/{contactUuid}` **operationId:** `updateContact` PATCH требует полную карточку по правилам POST: type, email, phone, страны, город, адрес, индекс и условные ФИО/организация. customer_uuid передавайте в JSON. Отсутствующие необязательные поля карточки обнуляются; external_id сохраняется, если не передан. Документ сохраняется при отсутствии обоих полей документа. Эта операция меняет локальную карточку, не выполняет EPP update; для домена используйте PUT /domains/{uuid}/contacts. **Scopes:** `contacts.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ### contactUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["type","email","phone","country_code","residence_country_code","city","address_line","postal_code"] | POST и PATCH используют одинаковую полную валидацию: PATCH не частичный. Неуказанные необязательные текстовые поля обнуляются; external_id сохраняется при отсутствии. Документ при отсутствии обоих полей сохраняется. customer_uuid обязателен в managed. Схема: CoreUpsertContactRequest | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Обязателен в managed. При aggregate можно опустить для агрегированного покупателя. | | `$.external_id` | string / null | Нет | maxLength: 255 | Идентификатор во внешнем биллинге; уникален для реселлера+среды. | | `$.type` | string | Да | enum: "person", "organization" | Тип контакта. | | `$.first_name` | string / null | Нет | maxLength: 255 | Обязателен для person. Unicode-буквы/диакритика, одиночные пробелы, дефис или апостроф между словами. | | `$.last_name` | string / null | Нет | maxLength: 255 | Обязателен для person; те же правила имени. | | `$.middle_name` | string / null | Нет | maxLength: 255 | В текущем API отчество ОБЯЗАТЕЛЬНО для person, не только имя и фамилия. | | `$.organization_name` | string / null | Нет | maxLength: 500 | Обязательно для organization. | | `$.email` | string | Да | format: "email"; maxLength: 255 | Email. | | `$.phone` | string | Да | maxLength: 50 | Телефон. Контроллер ограничивает длину, не проверяет E.164; используйте международный формат. | | `$.country_code` | string | Да | minLength: 2; maxLength: 2 | Страна адреса, верхний регистр сохраняется сервером. | | `$.residence_country_code` | string | Да | minLength: 2; maxLength: 2 | Обязательная страна резидентства; определяет правила документа. | | `$.city` | string | Да | maxLength: 255 | Город. | | `$.region` | string / null | Нет | maxLength: 255 | Регион. | | `$.address_line` | string | Да | maxLength: 1000 | Адрес. | | `$.postal_code` | string | Да | maxLength: 30 | Обязательный индекс. | | `$.external_id_type` | string / null | Нет | maxLength: 40 | Обязателен с external_id_value. KZ: BIN/IIN (12 цифр и контрольная сумма); RU: INN (10/12 цифр); US: EIN (NN-NNNNNNN), SSN (NNN-NN-NNNN); CN: USCC (18 символов); DE: VAT (DE+9 цифр); UZ: STIR (9), PINFL (14); GB: CRN (8), VAT (GB+9); TR: VKN (10), TCKN (11); UA: EDRPOU (8); AE: TRN (100+12). Для остальных стран PASSPORT/TAX_ID (непустое значение). USCC допускает [0-9A-HJ-NP-RT-UW-Y]{18}. Форматы регистрозависимы для значения документа, тип/страна нормализуются к верхнему регистру. | | `$.external_id_value` | string / null | Нет | maxLength: 100; Только запись | Обязателен с external_id_type; хранится, но не возвращается. Для KZ проверяется контрольная сумма; тестовые исключения зависят от конфигурации, test-ключ не отключает проверку. | | `$ (allOf 1)` | Условная схема | Да | | | | `$ (allOf 1) (if)` | object | Да | | | | `$ (allOf 1) (if).type` | Условная схема | Нет | const: "person" | | | `$ (allOf 1) (then)` | object | Да | required: ["first_name","last_name","middle_name"] | | | `$ (allOf 1) (then).first_name` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).last_name` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).middle_name` | string | Да | minLength: 1 | | | `$ (allOf 2)` | Условная схема | Да | | | | `$ (allOf 2) (if)` | object | Да | | | | `$ (allOf 2) (if).type` | Условная схема | Нет | const: "organization" | | | `$ (allOf 2) (then)` | object | Да | required: ["organization_name"] | | | `$ (allOf 2) (then).organization_name` | string | Да | minLength: 1 | | #### Полная замена с новым адресом ```json { "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 20", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CONTACT_UUID:?Set CONTACT_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/contacts/${CONTACT_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 20", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts/{contactUuid}"; path = path.replaceAll("{contactUuid}", encodeURIComponent(requiredEnv("CONTACT_UUID"))); const url = baseUrl + path; const body = `{ "type": "person", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 20", "postal_code": "050000", "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts/{contactUuid}" path = path.replace("{contactUuid}", quote(required_env("CONTACT_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 20\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts/{contactUuid}" value0 := requiredEnv("CONTACT_UUID") path = strings.ReplaceAll(path, "{contactUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 20\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts/{contactUuid}"; path = path.replace("{contactUuid}", URLEncoder.encode(requiredEnv("CONTACT_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 20\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts/{contactUuid}"; path = path.Replace("{contactUuid}", Uri.EscapeDataString(RequiredEnv("CONTACT_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"type\": \"person\",\n \"first_name\": \"Иван\",\n \"last_name\": \"Петров\",\n \"middle_name\": \"Иванович\",\n \"organization_name\": null,\n \"email\": \"ivan@example.com\",\n \"phone\": \"+77010000000\",\n \"country_code\": \"KZ\",\n \"residence_country_code\": \"KZ\",\n \"city\": \"Алматы\",\n \"region\": \"Алматы\",\n \"address_line\": \"ул. Примерная, 20\",\n \"postal_code\": \"050000\",\n \"external_id\": \"contact-1042\",\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Контакт не найден у выбранного покупателя/среды. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Передан частичный набор обязательных полей, невалидный документ либо занятый external_id. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreContactEnvelope | | `$.data` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at","external_id","customer_uuid"] | Локальный доменный контакт. external_id_value никогда не возвращается. Изменение этой карточки само по себе не выполняет EPP contact update. Схема: CoreContact | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.type` | string | Да | enum: "person", "organization" | Тип. | | `$.data.status` | string | Да | | Статус, при создании active. | | `$.data.first_name` | string / null | Да | | Имя. | | `$.data.last_name` | string / null | Да | | Фамилия. | | `$.data.middle_name` | string / null | Да | | Отчество. | | `$.data.organization_name` | string / null | Да | | Организация. | | `$.data.email` | string | Да | format: "email" | Email. | | `$.data.phone` | string | Да | | Телефон. | | `$.data.country_code` | string | Да | | Страна адреса. | | `$.data.residence_country_code` | string / null | Да | | Страна резидентства. | | `$.data.city` | string | Да | | Город. | | `$.data.region` | string / null | Да | | Регион. | | `$.data.address_line` | string | Да | | Адрес. | | `$.data.postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.external_id_type` | string / null | Да | | Тип документа, не идентификатор внешнего биллинга. | | `$.data.verification_status` | string | Да | | Статус верификации; создание обычно not_required. | | `$.data.is_locked_as_registrant` | boolean | Да | | Признак блокировки контакта как владельца. | | `$.data.verified_at` | string / null | Да | format: "date-time" | Время подтверждения. | | `$.data.external_id` | string / null | Да | | Идентификатор контакта во внешнем биллинге. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | #### Контакт с новым адресом (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000012", "type": "person", "status": "active", "first_name": "Иван", "last_name": "Петров", "middle_name": "Иванович", "organization_name": null, "email": "ivan@example.com", "phone": "+77010000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Алматы", "region": "Алматы", "address_line": "ул. Примерная, 20", "postal_code": "050000", "external_id_type": null, "verification_status": "not_required", "is_locked_as_registrant": false, "verified_at": null, "external_id": "contact-1042", "customer_uuid": "019a0000-0000-7000-8000-000000000010" } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Контакт не найден у выбранного покупателя/среды. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Передан частичный набор обязательных полей, невалидный документ либо занятый external_id. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/deleteContact.md # Удалить неиспользуемый контакт `DELETE /contacts/{contactUuid}` **operationId:** `deleteContact` Удаление запрещено, если контакт используется в owner/admin/tech/billing существующего домена или ожидающей регистрации. Не удаляет домены и не выполняет удаление EPP-контакта. customer_uuid передавайте query-параметром. Все данные ограничены текущим реселлером и средой ключа test/live. Смена среды параметром запроса не поддерживается. **Scopes:** `contacts.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ### contactUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### customer_uuid Расположение: `query`. Необязательный. Обязателен в managed; в aggregate отсутствие выбирает агрегированного покупателя. Неверный/чужой customer_uuid даёт 422, не пустой список. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string / null | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${CONTACT_UUID:?Set CONTACT_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request DELETE "$API_BASE_URL/contacts/${CONTACT_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" ``` ```php [PHP 8+] 'DELETE', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/contacts/{contactUuid}"; path = path.replaceAll("{contactUuid}", encodeURIComponent(requiredEnv("CONTACT_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "DELETE", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/contacts/{contactUuid}" path = path.replace("{contactUuid}", quote(required_env("CONTACT_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), } body = None request = Request(url, data=body, headers=headers, method="DELETE") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/contacts/{contactUuid}" value0 := requiredEnv("CONTACT_UUID") path = strings.ReplaceAll(path, "{contactUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("DELETE", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/contacts/{contactUuid}"; path = path.replace("{contactUuid}", URLEncoder.encode(requiredEnv("CONTACT_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .method("DELETE", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/contacts/{contactUuid}"; path = path.Replace("{contactUuid}", Uri.EscapeDataString(RequiredEnv("CONTACT_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Контакт используется существующим доменом или pending/provisioning регистрацией; также возможен конфликт Idempotency-Key. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | customer_uuid отсутствует в managed или не разрешён. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreContactDeletedEnvelope | | `$.data` | object | Да | required: ["uuid","deleted"] | Удаление локального контакта. Схема: CoreContactDeleted | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.deleted` | boolean | Да | const: true | Контакт удалён локально. Это не удаление домена в реестре. | #### Локальный контакт удалён (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000012", "deleted": true } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Scope, IP allowlist, режим или состояние реселлера не разрешает запрос. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Контакт используется существующим доменом или pending/provisioning регистрацией; также возможен конфликт Idempotency-Key. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 customer_uuid отсутствует в managed или не разрешён. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getBalance.md # Закупочный баланс реселлера `GET /balance` **operationId:** `getBalance` Только live. Возвращается активный закупочный финансовый счёт реселлера, не баланс конечного покупателя. Пополнение/списание произвольной суммы этим API не предусмотрено. held_minor означает резерв, не окончательное списание. **Scopes:** `balance.read` ## Параметры Параметры пути, строки запроса и дополнительные заголовки отсутствуют. ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/balance" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/balance"; const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/balance" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/balance" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/balance"; URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/balance"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Ключ test либо отсутствует balance.read. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Активный финансовый счёт реселлера или связанный счёт покупателя не найден. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreBalanceEnvelope | | `$.data` | object | Да | required: ["currency","available_minor","held_minor"] | Закупочный счёт реселлера, не баланс конечного клиента. Только live. Схема: CoreBalance | | `$.data.currency` | string | Да | | Валюта. | | `$.data.available_minor` | integer | Да | format: "int64" | Доступно. В минимальных единицах валюты, без десятичных дробей. | | `$.data.held_minor` | integer | Да | format: "int64" | Зарезервировано; не равно окончательно списанным средствам. В минимальных единицах валюты, без десятичных дробей. | #### Средства и резерв (200) ```json { "data": { "currency": "KZT", "available_minor": 9300000, "held_minor": 700000 } } ``` #### Нулевой баланс (200) ```json { "data": { "currency": "KZT", "available_minor": 0, "held_minor": 0 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Ключ test либо отсутствует balance.read. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Активный финансовый счёт реселлера или связанный счёт покупателя не найден. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listLedger.md # Проводки закупочного счёта `GET /ledger` **operationId:** `listLedger` Только live. Сортировка id по убыванию. Нет фильтров по дате, kind, валюте или customer_uuid. Остатки указаны после каждой проводки, amount_minor сам по себе недостаточен для восстановления движения между available и held. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `billing.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/ledger?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/ledger"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/ledger" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/ledger" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/ledger"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/ledger"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Ключ test либо отсутствует billing.read. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Активный закупочный счёт не найден. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Список и упрощённая пагинация. Схема: CoreLedgerEntryPage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","kind","amount_minor","currency","available_balance_minor","held_balance_minor","description","occurred_at"] | Проводка закупочного счёта. Возвращаются только публичные поля; ссылок на внутренние транзакции нет. Схема: CoreLedgerEntry | | `$.data[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].kind` | string | Да | | Вид проводки, например provider_reserve. Набор открыт; не вычисляйте баланс только по kind и amount_minor. | | `$.data[].amount_minor` | integer | Да | format: "int64" | Сумма операции. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].currency` | string | Да | | Валюта. | | `$.data[].available_balance_minor` | integer | Да | format: "int64" | Доступный остаток после операции. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].held_balance_minor` | integer | Да | format: "int64" | Зарезервированный остаток после операции. В минимальных единицах валюты, без десятичных дробей. | | `$.data[].description` | string / null | Да | | Описание. | | `$.data[].occurred_at` | string / null | Да | format: "date-time" | Время проводки. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Упрощённая курсорная пагинация. total, page и prev_cursor отсутствуют. Схема: CoreCursorMeta | | `$.meta.next_cursor` | string / null | Да | | Передать как cursor следующего запроса. null означает конец. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Фактический размер страницы. | #### Операции счёта (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000050", "kind": "provider_reserve", "amount_minor": 700000, "currency": "KZT", "available_balance_minor": 9300000, "held_balance_minor": 700000, "description": "Provider reserve.", "occurred_at": "2026-10-05T09:00:00+00:00" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Ключ test либо отсутствует billing.read. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Активный закупочный счёт не найден. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listTickets.md # Список обращений `GET /tickets` **operationId:** `listTickets` Только live. Публичные тикеты текущего реселлера, по last_message_at убыванию. В списке есть messages_count, но нет messages. Внутренние сообщения исключены. Пагинация cursor/per_page (по умолчанию 25, фактически ограничивается 1..100). Не поддерживаются page, offset, sort, include и произвольные фильтры. При следующем запросе передавайте исходные фильтры вместе с cursor; ссылки не гарантируют сохранение фильтров. **Scopes:** `tickets.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ### customer_uuid Расположение: `query`. Необязательный. Фильтр по покупателю. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### external_id Расположение: `query`. Необязательный. Точное совпадение внешнего ID. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### status Расположение: `query`. Необязательный. Точное состояние тикета. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/tickets?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/tickets"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/tickets" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/tickets" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/tickets"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/tickets"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Ключ test либо отсутствует tickets.read. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","links","meta"] | Список и ресурсная курсорная пагинация. Схема: CoreTicketSummaryPage | | `$.data` | array | Да | | Записи. | | `$.data[]` | object | Да | required: ["uuid","external_id","customer_uuid","number","subject","status","priority","department","category","service_uuid","domain_uuid","order_uuid","last_message_at","closed_at","messages_count","created_at","updated_at"] | Тикет в списке. Поле messages отсутствует, а не пустой массив. Схема: CoreTicketSummary | | `$.data[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data[].external_id` | string / null | Да | | ID тикета внешнего биллинга. | | `$.data[].customer_uuid` | string / null | Да | format: "uuid" | Покупатель. | | `$.data[].number` | string | Да | | Номер. | | `$.data[].subject` | string | Да | | Тема. | | `$.data[].status` | string | Да | | Состояние; ответ клиента открывает тикет, close_ticket=true закрывает. | | `$.data[].priority` | string | Да | enum: "low", "normal", "high", "urgent" | Приоритет. | | `$.data[].department` | string / null | Да | | Отдел. | | `$.data[].category` | string / null | Да | | Категория. | | `$.data[].service_uuid` | string / null | Да | format: "uuid" | Связанная услуга. | | `$.data[].domain_uuid` | string / null | Да | format: "uuid" | Связанный домен. | | `$.data[].order_uuid` | string / null | Да | format: "uuid" | Связанный заказ. | | `$.data[].last_message_at` | string / null | Да | format: "date-time" | Последнее сообщение. | | `$.data[].closed_at` | string / null | Да | format: "date-time" | Закрыт. | | `$.data[].messages_count` | integer | Да | format: "int64"; minimum: 0 | Количество публичных сообщений. | | `$.data[].created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data[].updated_at` | string / null | Да | format: "date-time" | Изменён. | | `$.links` | object | Да | required: ["first","last","prev","next"] | Навигационные ссылки. Курсорная пагинация не вычисляет первую/последнюю страницу. Сохраняйте исходные фильтры при переходе. Схема: CoreResourceLinks | | `$.links.first` | null | Да | | Всегда null. | | `$.links.last` | null | Да | | Всегда null. | | `$.links.prev` | string / null | Да | format: "uri" | Предыдущая страница или null. | | `$.links.next` | string / null | Да | format: "uri" | Следующая страница или null. | | `$.meta` | object | Да | required: ["path","per_page","next_cursor","prev_cursor"] | Пагинация Laravel Resource. Не содержит общего количества записей. Схема: CoreResourceMeta | | `$.meta.path` | string | Да | format: "uri" | URL коллекции. | | `$.meta.per_page` | integer | Да | format: "int64"; minimum: 1; maximum: 100 | Размер страницы. | | `$.meta.next_cursor` | string / null | Да | | Непрозрачный курсор следующей страницы. | | `$.meta.prev_cursor` | string / null | Да | | Непрозрачный курсор предыдущей страницы. | #### Тикет без массива messages (200) ```json { "data": [ { "uuid": "019a0000-0000-7000-8000-000000000060", "external_id": "support-4042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "TCK-20261005090000-DEMO42", "subject": "Проверка регистрации домена", "status": "open", "priority": "normal", "department": null, "category": null, "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030", "last_message_at": "2026-10-05T09:00:00+00:00", "closed_at": null, "messages_count": 1, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.com/api/reseller/v1/tickets", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` #### Нет записей в текущем контексте (200) ```json { "data": [], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.com/api/reseller/v1/tickets", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Ключ test либо отсутствует tickets.read. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Маршрут отключён конфигурацией либо объект недоступен в текущем контексте. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createTicket.md # Создать обращение `POST /tickets` **operationId:** `createTicket` Только live. customer_uuid обязателен в managed. Опциональные service_uuid/domain_uuid/order_uuid должны принадлежать тому же покупателю. Нужен владелец покупателя для авторства сообщения. Загрузка вложений в этом reseller API не реализована. **Scopes:** `tickets.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["subject","priority","message"] | Только live. У покупателя должен быть owner_user_id. Все привязанные объекты должны принадлежать этому покупателю и реселлеру. Схема: CoreCreateTicketRequest | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Обязателен в managed. | | `$.external_id` | string / null | Нет | maxLength: 255 | Уникален внутри реселлера. | | `$.subject` | string | Да | minLength: 3; maxLength: 500 | Тема. | | `$.priority` | string | Да | enum: "low", "normal", "high", "urgent" | Приоритет. | | `$.message` | string | Да | minLength: 3; maxLength: 5000 | Первое сообщение. | | `$.service_uuid` | string / null | Нет | format: "uuid" | Необязательная услуга. | | `$.domain_uuid` | string / null | Нет | format: "uuid" | Необязательный домен. | | `$.order_uuid` | string / null | Нет | format: "uuid" | Необязательный заказ. | #### Обращение о домене ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "support-4042", "subject": "Проверка регистрации домена", "priority": "normal", "message": "Просим проверить состояние регистрации домена reseller-example.kz.", "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/tickets" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "support-4042", "subject": "Проверка регистрации домена", "priority": "normal", "message": "Просим проверить состояние регистрации домена reseller-example.kz.", "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/tickets"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "external_id": "support-4042", "subject": "Проверка регистрации домена", "priority": "normal", "message": "Просим проверить состояние регистрации домена reseller-example.kz.", "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/tickets" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"support-4042\",\n \"subject\": \"Проверка регистрации домена\",\n \"priority\": \"normal\",\n \"message\": \"Просим проверить состояние регистрации домена reseller-example.kz.\",\n \"service_uuid\": \"019a0000-0000-7000-8000-000000000040\",\n \"domain_uuid\": \"019a0000-0000-7000-8000-000000000041\",\n \"order_uuid\": \"019a0000-0000-7000-8000-000000000030\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/tickets" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"support-4042\",\n \"subject\": \"Проверка регистрации домена\",\n \"priority\": \"normal\",\n \"message\": \"Просим проверить состояние регистрации домена reseller-example.kz.\",\n \"service_uuid\": \"019a0000-0000-7000-8000-000000000040\",\n \"domain_uuid\": \"019a0000-0000-7000-8000-000000000041\",\n \"order_uuid\": \"019a0000-0000-7000-8000-000000000030\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/tickets"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"support-4042\",\n \"subject\": \"Проверка регистрации домена\",\n \"priority\": \"normal\",\n \"message\": \"Просим проверить состояние регистрации домена reseller-example.kz.\",\n \"service_uuid\": \"019a0000-0000-7000-8000-000000000040\",\n \"domain_uuid\": \"019a0000-0000-7000-8000-000000000041\",\n \"order_uuid\": \"019a0000-0000-7000-8000-000000000030\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/tickets"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"external_id\": \"support-4042\",\n \"subject\": \"Проверка регистрации домена\",\n \"priority\": \"normal\",\n \"message\": \"Просим проверить состояние регистрации домена reseller-example.kz.\",\n \"service_uuid\": \"019a0000-0000-7000-8000-000000000040\",\n \"domain_uuid\": \"019a0000-0000-7000-8000-000000000041\",\n \"order_uuid\": \"019a0000-0000-7000-8000-000000000030\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Без привязки к услуге ```json { "customer_uuid": "019a0000-0000-7000-8000-000000000010", "subject": "Вопрос по подключению", "priority": "normal", "message": "Просим уточнить условия подключения." } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/tickets" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "subject": "Вопрос по подключению", "priority": "normal", "message": "Просим уточнить условия подключения." }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/tickets"; const url = baseUrl + path; const body = `{ "customer_uuid": "019a0000-0000-7000-8000-000000000010", "subject": "Вопрос по подключению", "priority": "normal", "message": "Просим уточнить условия подключения." }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/tickets" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"subject\": \"Вопрос по подключению\",\n \"priority\": \"normal\",\n \"message\": \"Просим уточнить условия подключения.\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/tickets" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"subject\": \"Вопрос по подключению\",\n \"priority\": \"normal\",\n \"message\": \"Просим уточнить условия подключения.\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/tickets"; URI uri = URI.create(baseUrl + path); String body = "{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"subject\": \"Вопрос по подключению\",\n \"priority\": \"normal\",\n \"message\": \"Просим уточнить условия подключения.\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/tickets"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"customer_uuid\": \"019a0000-0000-7000-8000-000000000010\",\n \"subject\": \"Вопрос по подключению\",\n \"priority\": \"normal\",\n \"message\": \"Просим уточнить условия подключения.\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Ключ test либо отсутствует tickets.write. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Связанный домен/заказ/услуга не найдены у покупателя и реселлера. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Неверные длины/приоритет; customer_uuid не найден; нет владельца покупателя; внешний ID уже занят. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreTicketEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","subject","status","priority","department","category","service_uuid","domain_uuid","order_uuid","last_message_at","closed_at","messages_count","created_at","updated_at","messages"] | Тикет с полной публичной перепиской в хронологическом порядке. Схема: CoreTicket | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID тикета внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | Покупатель. | | `$.data.number` | string | Да | | Номер. | | `$.data.subject` | string | Да | | Тема. | | `$.data.status` | string | Да | | Состояние; ответ клиента открывает тикет, close_ticket=true закрывает. | | `$.data.priority` | string | Да | enum: "low", "normal", "high", "urgent" | Приоритет. | | `$.data.department` | string / null | Да | | Отдел. | | `$.data.category` | string / null | Да | | Категория. | | `$.data.service_uuid` | string / null | Да | format: "uuid" | Связанная услуга. | | `$.data.domain_uuid` | string / null | Да | format: "uuid" | Связанный домен. | | `$.data.order_uuid` | string / null | Да | format: "uuid" | Связанный заказ. | | `$.data.last_message_at` | string / null | Да | format: "date-time" | Последнее сообщение. | | `$.data.closed_at` | string / null | Да | format: "date-time" | Закрыт. | | `$.data.messages_count` | integer | Да | format: "int64"; minimum: 0 | Количество публичных сообщений. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | | `$.data.messages` | array | Да | | Сообщения от старых к новым, без отдельной пагинации. | | `$.data.messages[]` | object | Да | required: ["uuid","type","body","sender_type","created_at","attachments"] | Только публичное сообщение; внутренние заметки не возвращаются. Схема: CoreTicketMessage | | `$.data.messages[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.messages[].type` | string | Да | | Тип сообщения. | | `$.data.messages[].body` | string | Да | | Текст. | | `$.data.messages[].sender_type` | string | Да | enum: "customer", "support" | Сторона. | | `$.data.messages[].created_at` | string / null | Да | format: "date-time" | Создано. | | `$.data.messages[].attachments` | array | Да | | Метаданные вложений. | | `$.data.messages[].attachments[]` | object | Да | required: ["uuid","name","mime_type","size_bytes"] | Метаданные вложения. Этот API не предоставляет upload/download endpoint или URL содержимого. Схема: CoreTicketAttachment | | `$.data.messages[].attachments[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.messages[].attachments[].name` | string | Да | | Исходное имя. | | `$.data.messages[].attachments[].mime_type` | string | Да | | MIME. | | `$.data.messages[].attachments[].size_bytes` | integer | Да | format: "int64"; minimum: 0 | Байты. | #### Открытое обращение с первым сообщением (201) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000060", "external_id": "support-4042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "TCK-20261005090000-DEMO42", "subject": "Проверка регистрации домена", "status": "open", "priority": "normal", "department": null, "category": null, "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030", "last_message_at": "2026-10-05T09:00:00+00:00", "closed_at": null, "messages_count": 1, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00", "messages": [ { "uuid": "019a0000-0000-7000-8000-000000000061", "type": "message", "body": "Просим проверить состояние регистрации домена reseller-example.kz.", "sender_type": "customer", "created_at": "2026-10-05T09:00:00+00:00", "attachments": [] } ] } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Ключ test либо отсутствует tickets.write. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Связанный домен/заказ/услуга не найдены у покупателя и реселлера. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Неверные длины/приоритет; customer_uuid не найден; нет владельца покупателя; внешний ID уже занят. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getTicket.md # Получить тикет и переписку `GET /tickets/{ticketUuid}` **operationId:** `getTicket` Только live. Включает все публичные сообщения от старых к новым и метаданные вложений. Внутренние заметки исключены; download URL вложений не выдаются. **Scopes:** `tickets.read` ## Параметры ### ticketUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${TICKET_UUID:?Set TICKET_UUID}" curl --request GET "$API_BASE_URL/tickets/${TICKET_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/tickets/{ticketUuid}"; path = path.replaceAll("{ticketUuid}", encodeURIComponent(requiredEnv("TICKET_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/tickets/{ticketUuid}" path = path.replace("{ticketUuid}", quote(required_env("TICKET_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/tickets/{ticketUuid}" value0 := requiredEnv("TICKET_UUID") path = strings.ReplaceAll(path, "{ticketUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/tickets/{ticketUuid}"; path = path.replace("{ticketUuid}", URLEncoder.encode(requiredEnv("TICKET_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/tickets/{ticketUuid}"; path = path.Replace("{ticketUuid}", Uri.EscapeDataString(RequiredEnv("TICKET_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Ключ test либо отсутствует tickets.read. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Тикет не найден у текущего реселлера. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 422 | `validation_failed` | Параметры не прошли валидацию или не выполнены бизнес-условия. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreTicketEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","subject","status","priority","department","category","service_uuid","domain_uuid","order_uuid","last_message_at","closed_at","messages_count","created_at","updated_at","messages"] | Тикет с полной публичной перепиской в хронологическом порядке. Схема: CoreTicket | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID тикета внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | Покупатель. | | `$.data.number` | string | Да | | Номер. | | `$.data.subject` | string | Да | | Тема. | | `$.data.status` | string | Да | | Состояние; ответ клиента открывает тикет, close_ticket=true закрывает. | | `$.data.priority` | string | Да | enum: "low", "normal", "high", "urgent" | Приоритет. | | `$.data.department` | string / null | Да | | Отдел. | | `$.data.category` | string / null | Да | | Категория. | | `$.data.service_uuid` | string / null | Да | format: "uuid" | Связанная услуга. | | `$.data.domain_uuid` | string / null | Да | format: "uuid" | Связанный домен. | | `$.data.order_uuid` | string / null | Да | format: "uuid" | Связанный заказ. | | `$.data.last_message_at` | string / null | Да | format: "date-time" | Последнее сообщение. | | `$.data.closed_at` | string / null | Да | format: "date-time" | Закрыт. | | `$.data.messages_count` | integer | Да | format: "int64"; minimum: 0 | Количество публичных сообщений. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | | `$.data.messages` | array | Да | | Сообщения от старых к новым, без отдельной пагинации. | | `$.data.messages[]` | object | Да | required: ["uuid","type","body","sender_type","created_at","attachments"] | Только публичное сообщение; внутренние заметки не возвращаются. Схема: CoreTicketMessage | | `$.data.messages[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.messages[].type` | string | Да | | Тип сообщения. | | `$.data.messages[].body` | string | Да | | Текст. | | `$.data.messages[].sender_type` | string | Да | enum: "customer", "support" | Сторона. | | `$.data.messages[].created_at` | string / null | Да | format: "date-time" | Создано. | | `$.data.messages[].attachments` | array | Да | | Метаданные вложений. | | `$.data.messages[].attachments[]` | object | Да | required: ["uuid","name","mime_type","size_bytes"] | Метаданные вложения. Этот API не предоставляет upload/download endpoint или URL содержимого. Схема: CoreTicketAttachment | | `$.data.messages[].attachments[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.messages[].attachments[].name` | string | Да | | Исходное имя. | | `$.data.messages[].attachments[].mime_type` | string | Да | | MIME. | | `$.data.messages[].attachments[].size_bytes` | integer | Да | format: "int64"; minimum: 0 | Байты. | #### Открытый тикет (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000060", "external_id": "support-4042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "TCK-20261005090000-DEMO42", "subject": "Проверка регистрации домена", "status": "open", "priority": "normal", "department": null, "category": null, "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030", "last_message_at": "2026-10-05T09:00:00+00:00", "closed_at": null, "messages_count": 1, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T09:00:00+00:00", "messages": [ { "uuid": "019a0000-0000-7000-8000-000000000061", "type": "message", "body": "Просим проверить состояние регистрации домена reseller-example.kz.", "sender_type": "customer", "created_at": "2026-10-05T09:00:00+00:00", "attachments": [] } ] } } ``` #### Закрытый тикет (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000060", "external_id": "support-4042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "TCK-20261005090000-DEMO42", "subject": "Проверка регистрации домена", "status": "closed", "priority": "normal", "department": null, "category": null, "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030", "last_message_at": "2026-10-05T10:00:00+00:00", "closed_at": "2026-10-05T10:00:00+00:00", "messages_count": 2, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T10:00:00+00:00", "messages": [ { "uuid": "019a0000-0000-7000-8000-000000000061", "type": "message", "body": "Просим проверить состояние регистрации домена reseller-example.kz.", "sender_type": "customer", "created_at": "2026-10-05T09:00:00+00:00", "attachments": [] }, { "uuid": "019a0000-0000-7000-8000-000000000062", "type": "message", "body": "Вопрос решён, спасибо.", "sender_type": "customer", "created_at": "2026-10-05T10:00:00+00:00", "attachments": [] } ] } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Ключ test либо отсутствует tickets.read. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Тикет не найден у текущего реселлера. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Параметры не прошли валидацию или не выполнены бизнес-условия. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/replyTicket.md # Ответить и при необходимости закрыть тикет `POST /tickets/{ticketUuid}/messages` **operationId:** `replyTicket` Только live. Создаётся публичное сообщение от владельца покупателя. close_ticket=true закрывает, false/отсутствие открывает тикет (включая ранее закрытый). Возвращается полная карточка тикета, не только созданное сообщение. **Scopes:** `tickets.write` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ### ticketUuid Расположение: `path`. Обязательный. UUID ресурса текущего реселлера. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["message"] | Добавляет публичное сообщение. Нет отдельного endpoint изменения приоритета, назначения отдела или закрытия без сообщения. Схема: CoreReplyTicketRequest | | `$.message` | string | Да | minLength: 1; maxLength: 5000 | Сообщение. | | `$.close_ticket` | boolean | Нет | default: false | true закрывает тикет; false открывает его, в том числе ранее закрытый. | #### Ответить ```json { "message": "Прилагаем уточнение: ошибка повторяется." } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${TICKET_UUID:?Set TICKET_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/tickets/${TICKET_UUID}/messages" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "message": "Прилагаем уточнение: ошибка повторяется." }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/tickets/{ticketUuid}/messages"; path = path.replaceAll("{ticketUuid}", encodeURIComponent(requiredEnv("TICKET_UUID"))); const url = baseUrl + path; const body = `{ "message": "Прилагаем уточнение: ошибка повторяется." }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/tickets/{ticketUuid}/messages" path = path.replace("{ticketUuid}", quote(required_env("TICKET_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"message\": \"Прилагаем уточнение: ошибка повторяется.\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/tickets/{ticketUuid}/messages" value0 := requiredEnv("TICKET_UUID") path = strings.ReplaceAll(path, "{ticketUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"message\": \"Прилагаем уточнение: ошибка повторяется.\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/tickets/{ticketUuid}/messages"; path = path.replace("{ticketUuid}", URLEncoder.encode(requiredEnv("TICKET_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"message\": \"Прилагаем уточнение: ошибка повторяется.\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/tickets/{ticketUuid}/messages"; path = path.Replace("{ticketUuid}", Uri.EscapeDataString(RequiredEnv("TICKET_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"message\": \"Прилагаем уточнение: ошибка повторяется.\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Ответить и закрыть ```json { "message": "Вопрос решён, спасибо.", "close_ticket": true } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${TICKET_UUID:?Set TICKET_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/tickets/${TICKET_UUID}/messages" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "message": "Вопрос решён, спасибо.", "close_ticket": true }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/tickets/{ticketUuid}/messages"; path = path.replaceAll("{ticketUuid}", encodeURIComponent(requiredEnv("TICKET_UUID"))); const url = baseUrl + path; const body = `{ "message": "Вопрос решён, спасибо.", "close_ticket": true }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/tickets/{ticketUuid}/messages" path = path.replace("{ticketUuid}", quote(required_env("TICKET_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"message\": \"Вопрос решён, спасибо.\",\n \"close_ticket\": true\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/tickets/{ticketUuid}/messages" value0 := requiredEnv("TICKET_UUID") path = strings.ReplaceAll(path, "{ticketUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"message\": \"Вопрос решён, спасибо.\",\n \"close_ticket\": true\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/tickets/{ticketUuid}/messages"; path = path.replace("{ticketUuid}", URLEncoder.encode(requiredEnv("TICKET_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"message\": \"Вопрос решён, спасибо.\",\n \"close_ticket\": true\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/tickets/{ticketUuid}/messages"; path = path.Replace("{ticketUuid}", Uri.EscapeDataString(RequiredEnv("TICKET_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"message\": \"Вопрос решён, спасибо.\",\n \"close_ticket\": true\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Bearer-ключ отсутствует, недействителен, отозван или истёк. | Проверьте ключ и его срок, не повторяйте с теми же неверными credentials. | | 403 | `forbidden` | Ключ test либо отсутствует tickets.write. | Проверьте scopes, среду, allowlist и настройки реселлера через администратора. | | 404 | `resource_not_found` | Тикет не найден у реселлера. | Проверьте UUID, среду и принадлежность; при отключённой функции обратитесь к оператору. | | 409 | `conflict` | Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. | Для того же запроса дождитесь завершения и повторите неизменённый запрос с тем же ключом; изменённый запрос требует нового ключа после сверки результата. | | 422 | `validation_failed` | Пустое/слишком длинное сообщение, неверный boolean или у покупателя нет владельца. | Исправьте поля из error.details/условия. Не повторяйте неизменённый невалидный запрос. | | 429 | `rate_limit_exceeded` | Лимит текущего API-ключа исчерпан. | Учитывайте Retry-After при наличии, используйте backoff с jitter. Запись повторяйте с прежним Idempotency-Key и неизменённым телом. | | 500 | `internal_error` | Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. | Сохраните request_id; сначала проверьте состояние объекта, затем повторяйте запись с тем же ключом. Для неоднозначного результата обращайтесь в поддержку. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreTicketEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","subject","status","priority","department","category","service_uuid","domain_uuid","order_uuid","last_message_at","closed_at","messages_count","created_at","updated_at","messages"] | Тикет с полной публичной перепиской в хронологическом порядке. Схема: CoreTicket | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID тикета внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | Покупатель. | | `$.data.number` | string | Да | | Номер. | | `$.data.subject` | string | Да | | Тема. | | `$.data.status` | string | Да | | Состояние; ответ клиента открывает тикет, close_ticket=true закрывает. | | `$.data.priority` | string | Да | enum: "low", "normal", "high", "urgent" | Приоритет. | | `$.data.department` | string / null | Да | | Отдел. | | `$.data.category` | string / null | Да | | Категория. | | `$.data.service_uuid` | string / null | Да | format: "uuid" | Связанная услуга. | | `$.data.domain_uuid` | string / null | Да | format: "uuid" | Связанный домен. | | `$.data.order_uuid` | string / null | Да | format: "uuid" | Связанный заказ. | | `$.data.last_message_at` | string / null | Да | format: "date-time" | Последнее сообщение. | | `$.data.closed_at` | string / null | Да | format: "date-time" | Закрыт. | | `$.data.messages_count` | integer | Да | format: "int64"; minimum: 0 | Количество публичных сообщений. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | | `$.data.messages` | array | Да | | Сообщения от старых к новым, без отдельной пагинации. | | `$.data.messages[]` | object | Да | required: ["uuid","type","body","sender_type","created_at","attachments"] | Только публичное сообщение; внутренние заметки не возвращаются. Схема: CoreTicketMessage | | `$.data.messages[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.messages[].type` | string | Да | | Тип сообщения. | | `$.data.messages[].body` | string | Да | | Текст. | | `$.data.messages[].sender_type` | string | Да | enum: "customer", "support" | Сторона. | | `$.data.messages[].created_at` | string / null | Да | format: "date-time" | Создано. | | `$.data.messages[].attachments` | array | Да | | Метаданные вложений. | | `$.data.messages[].attachments[]` | object | Да | required: ["uuid","name","mime_type","size_bytes"] | Метаданные вложения. Этот API не предоставляет upload/download endpoint или URL содержимого. Схема: CoreTicketAttachment | | `$.data.messages[].attachments[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.messages[].attachments[].name` | string | Да | | Исходное имя. | | `$.data.messages[].attachments[].mime_type` | string | Да | | MIME. | | `$.data.messages[].attachments[].size_bytes` | integer | Да | format: "int64"; minimum: 0 | Байты. | #### Ответ закрыл обращение (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000060", "external_id": "support-4042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "TCK-20261005090000-DEMO42", "subject": "Проверка регистрации домена", "status": "closed", "priority": "normal", "department": null, "category": null, "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030", "last_message_at": "2026-10-05T10:00:00+00:00", "closed_at": "2026-10-05T10:00:00+00:00", "messages_count": 2, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T10:00:00+00:00", "messages": [ { "uuid": "019a0000-0000-7000-8000-000000000061", "type": "message", "body": "Просим проверить состояние регистрации домена reseller-example.kz.", "sender_type": "customer", "created_at": "2026-10-05T09:00:00+00:00", "attachments": [] }, { "uuid": "019a0000-0000-7000-8000-000000000062", "type": "message", "body": "Вопрос решён, спасибо.", "sender_type": "customer", "created_at": "2026-10-05T10:00:00+00:00", "attachments": [] } ] } } ``` #### Ответ без close_ticket открывает тикет (200) ```json { "data": { "uuid": "019a0000-0000-7000-8000-000000000060", "external_id": "support-4042", "customer_uuid": "019a0000-0000-7000-8000-000000000010", "number": "TCK-20261005090000-DEMO42", "subject": "Проверка регистрации домена", "status": "open", "priority": "normal", "department": null, "category": null, "service_uuid": "019a0000-0000-7000-8000-000000000040", "domain_uuid": "019a0000-0000-7000-8000-000000000041", "order_uuid": "019a0000-0000-7000-8000-000000000030", "last_message_at": "2026-10-05T10:00:00+00:00", "closed_at": null, "messages_count": 2, "created_at": "2026-10-05T09:00:00+00:00", "updated_at": "2026-10-05T10:00:00+00:00", "messages": [ { "uuid": "019a0000-0000-7000-8000-000000000061", "type": "message", "body": "Просим проверить состояние регистрации домена reseller-example.kz.", "sender_type": "customer", "created_at": "2026-10-05T09:00:00+00:00", "attachments": [] }, { "uuid": "019a0000-0000-7000-8000-000000000062", "type": "message", "body": "Вопрос решён, спасибо.", "sender_type": "customer", "created_at": "2026-10-05T10:00:00+00:00", "attachments": [] } ] } } ``` ### HTTP 401 Bearer-ключ отсутствует, недействителен, отозван или истёк. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Ключ test либо отсутствует tickets.write. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Тикет не найден у реселлера. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Idempotency-Key уже обрабатывается либо использован для другого метода, URL/query или тела. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Пустое/слишком длинное сообщение, неверный boolean или у покупателя нет владельца. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Превышен общий лимит текущего API-ключа по всем методам (в том числе replay). Квота отдельных EPP-команд и фоновых задач не равна этому HTTP-лимиту. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Необработанная ошибка выполнения; это не подтверждение отсутствия побочного эффекта. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listDomains.md # Список доменов `GET /domains` **operationId:** `listDomains` Требуемые scopes: domains.read. Возвращает локальные записи только своего провайдера в окружении ключа; сортировка id по убыванию. GET не обращается к реестру. Soft-deleted домены и записи под глобальным migration hold не возвращаются. Фильтры точного совпадения; registry_status и sort не поддерживаются. per_page приводится к integer и ограничивается диапазоном 1..100 (по умолчанию 25). Передавайте непрозрачный cursor без изменений; отсутствие next_cursor означает конец списка. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ### customer_uuid Расположение: `query`. Необязательный. Точное совпадение UUID клиента. Невалидный UUID не имеет отдельной валидации; передавайте корректный UUID. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### external_id Расположение: `query`. Необязательный. Точное совпадение внешнего идентификатора домена. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### status Расположение: `query`. Необязательный. Локальный status, например active; не EPP status. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### registration_status Расположение: `query`. Необязательный. Локальный registration_status, например registered. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/domains?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните доступность API и base URL у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","links","meta"] | Схема: DomainListEnvelope | | `$.data` | array | Да | | Домены текущего провайдера и окружения, id по убыванию. | | `$.data[]` | object | Да | required: ["uuid","external_id","service_uuid","customer_uuid","domain_name","unicode_domain_name","punycode_domain_name","status","registration_status","registrar_operation_status","registry_status","verification_status","verification_registry_status","edu_license_status","hold_status","hold_reason","registered_at","expires_at","last_synced_at","zone","contacts","nameservers","created_at","updated_at"] | Схема: DomainRecord | | `$.data[].uuid` | string | Да | format: "uuid" | UUID домена; используйте в domainUuid. | | `$.data[].external_id` | string / null | Да | | Идентификатор услуги во внешнем биллинге; null для неназначенного. | | `$.data[].service_uuid` | string / null | Да | format: "uuid" | UUID услуги для GET /services/{serviceUuid}. | | `$.data[].customer_uuid` | string / null | Да | format: "uuid" | UUID плательщика. | | `$.data[].domain_name` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.data[].unicode_domain_name` | string / null | Да | | Unicode-представление имени. | | `$.data[].punycode_domain_name` | string / null | Да | | ASCII/Punycode-представление имени. | | `$.data[].status` | string / null | Да | | Локальное состояние услуги: например active, pending, expired. Не статус EPP. | | `$.data[].registration_status` | string / null | Да | | Локальное состояние регистрации: например pending, registered, failed. | | `$.data[].registrar_operation_status` | string / null | Да | | Последний результат операции регистратора. | | `$.data[].registry_status` | string / null | Да | | Сводное состояние реестра; строка не ограничена enum и не является массивом EPP-статусов. | | `$.data[].verification_status` | string / null | Да | | Локальное состояние проверки владельца. | | `$.data[].verification_registry_status` | string / null | Да | | Состояние проверки владельца в реестре. | | `$.data[].edu_license_status` | string / null | Да | | Состояние образовательной лицензии для соответствующих зон. | | `$.data[].hold_status` | string / null | Да | | Локальное состояние блокировки. | | `$.data[].hold_reason` | string / null | Да | | Причина блокировки; может быть null. | | `$.data[].registered_at` | string / null | Да | format: "date-time" | Дата создания в реестре; используется для ограничения продления 24 часа. | | `$.data[].expires_at` | string / null | Да | format: "date-time" | Актуальное локальное время окончания; подтверждайте синхронизацией. | | `$.data[].last_synced_at` | string / null | Да | format: "date-time" | Последняя синхронизация с реестром. | | `$.data[].zone` | anyOf | Да | | Зона или null. | | `$.data[].zone (anyOf 1)` | object | Да | required: ["uuid","zone","display_name"] | | | `$.data[].zone (anyOf 1).uuid` | string | Да | format: "uuid" | UUID зоны для расчёта quote. | | `$.data[].zone (anyOf 1).zone` | string | Да | | Зона без ведущей точки. | | `$.data[].zone (anyOf 1).display_name` | string / null | Да | | Отображаемое название зоны. | | `$.data[].zone (anyOf 2)` | null | Да | | | | `$.data[].contacts` | object | Да | required: ["owner","admin","tech","billing"] | | | `$.data[].contacts.owner` | anyOf | Да | | Контакт роли owner или null. | | `$.data[].contacts.owner (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data[].contacts.owner (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data[].contacts.owner (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data[].contacts.owner (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data[].contacts.owner (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data[].contacts.owner (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data[].contacts.owner (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data[].contacts.owner (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data[].contacts.owner (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data[].contacts.owner (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data[].contacts.owner (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data[].contacts.owner (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data[].contacts.owner (anyOf 1).city` | string / null | Да | | Город. | | `$.data[].contacts.owner (anyOf 1).region` | string / null | Да | | Регион. | | `$.data[].contacts.owner (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data[].contacts.owner (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data[].contacts.owner (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data[].contacts.owner (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data[].contacts.owner (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data[].contacts.owner (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data[].contacts.owner (anyOf 2)` | null | Да | | | | `$.data[].contacts.admin` | anyOf | Да | | Контакт роли admin или null. | | `$.data[].contacts.admin (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data[].contacts.admin (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data[].contacts.admin (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data[].contacts.admin (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data[].contacts.admin (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data[].contacts.admin (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data[].contacts.admin (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data[].contacts.admin (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data[].contacts.admin (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data[].contacts.admin (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data[].contacts.admin (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data[].contacts.admin (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data[].contacts.admin (anyOf 1).city` | string / null | Да | | Город. | | `$.data[].contacts.admin (anyOf 1).region` | string / null | Да | | Регион. | | `$.data[].contacts.admin (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data[].contacts.admin (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data[].contacts.admin (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data[].contacts.admin (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data[].contacts.admin (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data[].contacts.admin (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data[].contacts.admin (anyOf 2)` | null | Да | | | | `$.data[].contacts.tech` | anyOf | Да | | Контакт роли tech или null. | | `$.data[].contacts.tech (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data[].contacts.tech (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data[].contacts.tech (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data[].contacts.tech (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data[].contacts.tech (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data[].contacts.tech (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data[].contacts.tech (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data[].contacts.tech (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data[].contacts.tech (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data[].contacts.tech (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data[].contacts.tech (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data[].contacts.tech (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data[].contacts.tech (anyOf 1).city` | string / null | Да | | Город. | | `$.data[].contacts.tech (anyOf 1).region` | string / null | Да | | Регион. | | `$.data[].contacts.tech (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data[].contacts.tech (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data[].contacts.tech (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data[].contacts.tech (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data[].contacts.tech (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data[].contacts.tech (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data[].contacts.tech (anyOf 2)` | null | Да | | | | `$.data[].contacts.billing` | anyOf | Да | | Контакт роли billing или null. | | `$.data[].contacts.billing (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data[].contacts.billing (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data[].contacts.billing (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data[].contacts.billing (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data[].contacts.billing (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data[].contacts.billing (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data[].contacts.billing (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data[].contacts.billing (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data[].contacts.billing (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data[].contacts.billing (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data[].contacts.billing (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data[].contacts.billing (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data[].contacts.billing (anyOf 1).city` | string / null | Да | | Город. | | `$.data[].contacts.billing (anyOf 1).region` | string / null | Да | | Регион. | | `$.data[].contacts.billing (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data[].contacts.billing (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data[].contacts.billing (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data[].contacts.billing (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data[].contacts.billing (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data[].contacts.billing (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data[].contacts.billing (anyOf 2)` | null | Да | | | | `$.data[].nameservers` | array | Да | | Текущие локальные NS; массив может быть пустым. | | `$.data[].nameservers[]` | object | Да | required: ["hostname","ipv4","ipv6"] | | | `$.data[].nameservers[].hostname` | string | Да | | Имя сервера. | | `$.data[].nameservers[].ipv4` | string / null | Да | format: "ipv4" | Glue IPv4. | | `$.data[].nameservers[].ipv6` | string / null | Да | format: "ipv6" | Glue IPv6. | | `$.data[].created_at` | string / null | Да | format: "date-time" | Создание локальной записи. | | `$.data[].updated_at` | string / null | Да | format: "date-time" | Последнее изменение локальной записи. | | `$.links` | object | Да | required: ["first","last","prev","next"] | Схема: DomainCursorLinks | | `$.links.first` | string / null | Да | | Для cursor pagination null. | | `$.links.last` | string / null | Да | | Для cursor pagination null. | | `$.links.prev` | string / null | Да | format: "uri" | URL предыдущей страницы либо null. | | `$.links.next` | string / null | Да | format: "uri" | URL следующей страницы либо null. | | `$.meta` | object | Да | required: ["path","per_page","next_cursor","prev_cursor"] | Схема: DomainCursorMeta | | `$.meta.path` | string | Да | format: "uri" | URL списка без cursor. | | `$.meta.per_page` | integer | Да | minimum: 1; maximum: 100 | Фактический размер страницы. | | `$.meta.next_cursor` | string / null | Да | | Непрозрачный cursor следующей страницы. | | `$.meta.prev_cursor` | string / null | Да | | Непрозрачный cursor предыдущей страницы. | #### Последняя страница с одним доменом (200) ```json { "data": [ { "uuid": "019a1234-1000-7000-8000-000000000001", "external_id": "crm-domain-1001", "service_uuid": "019a1234-1000-7000-8000-000000000005", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "domain_name": "example.kz", "unicode_domain_name": "example.kz", "punycode_domain_name": "example.kz", "status": "active", "registration_status": "registered", "registrar_operation_status": "success", "registry_status": "ok", "verification_status": "pending", "verification_registry_status": null, "edu_license_status": null, "hold_status": "none", "hold_reason": null, "registered_at": "2026-10-01T08:00:00+00:00", "expires_at": "2027-10-01T08:00:00+00:00", "last_synced_at": "2026-10-05T08:00:00+00:00", "zone": { "uuid": "019a1234-1000-7000-8000-000000000004", "zone": "kz", "display_name": ".kz" }, "contacts": { "owner": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null }, "admin": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null }, "tech": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null }, "billing": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null } }, "nameservers": [ { "hostname": "ns1.example.net", "ipv4": null, "ipv6": null }, { "hostname": "ns2.example.net", "ipv4": null, "ipv6": null } ], "created_at": "2026-10-01T07:59:00+00:00", "updated_at": "2026-10-05T08:00:00+00:00" } ], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.net/api/reseller/v1/domains", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` #### Нет подходящих доменов (200) ```json { "data": [], "links": { "first": null, "last": null, "prev": null, "next": null }, "meta": { "path": "https://api.example.net/api/reseller/v1/domains", "per_page": 25, "next_cursor": null, "prev_cursor": null } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/domains" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainController.php` ::: --- SOURCE: /reference/registerDomain.md # Создать заказ регистрации домена `POST /domains` **operationId:** `registerDomain` Требуемые scopes: orders.create, domains.register. Это alias OrderController::store, а не очередь reseller_domain_operation. Предварительно создайте контакты (contacts.write), запросите quote (quotes.create) с resource_type=domain_zone, operation=register, quantity=1, period_unit=year, period_count=1..10. Payload quote содержит domain_name, четыре contact_uuids и, при необходимости, nameservers 2..6, purpose, external_id, notes, whois_privacy_enabled. Контакты должны быть active и принадлежать клиенту, провайдеру и окружению; зона должна соответствовать имени. Для managed передайте customer_uuid; aggregate использует назначенного клиента. Quote должен быть active, не просрочен, создан этим API-ключом и ещё не использован. external_id запроса идентифицирует заказ; external_id в payload quote идентифицирует услугу. В external checkout заказ рассчитывается и запускается из оптового баланса; platform checkout требует отдельной оплаты счёта. test не расходует live-баланс, но требует отдельного тестового маршрута регистратора. HTTP 201 возвращает Order, не Domain и не Operation. Отслеживайте заказ через /orders/{orderUuid}, услугу /services/{serviceUuid} и домен через список; нужны orders.read/services.read/domains.read. Задача register_domain недоступна через /operations. Цена 500000 в примере условна. При неопределённой регистрации не оформляйте второй заказ; требуется сверка существующего. Контроллер общий с /orders: фактически может принять также товарные позиции, при наличии дополнительных scopes (например hosting.order); для доменной интеграции отправляйте только domain_zone/register. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `orders.create`, `domains.register` **x-prerequisite-scopes:** ```json [ { "scope": "quotes.create", "when": "Для выпуска quote до вызова этого endpoint." }, { "scope": "contacts.write", "when": "Если контактные профили ещё не созданы." }, { "scope": "domains.read", "when": "Для последующего чтения домена/операции." } ] ``` **x-prerequisite-request-example:** ```json { "description": "Предварительный запрос POST /quotes (другой endpoint, scope quotes.create, отдельный Idempotency-Key). UUID из data.uuid полученного quote передайте в quote_uuid текущей операции. Не используйте здесь демонстрационные UUID или имя без замены.", "method": "POST", "path": "/quotes", "body": { "customer_uuid": "019a1234-1000-7000-8000-000000000002", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a1234-1000-7000-8000-000000000004", "operation": "register", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "example.kz", "contact_uuids": { "owner": "019a1234-1000-7000-8000-000000000003", "admin": "019a1234-1000-7000-8000-000000000003", "tech": "019a1234-1000-7000-8000-000000000003", "billing": "019a1234-1000-7000-8000-000000000003" }, "nameservers": [ { "hostname": "ns1.example.net" }, { "hostname": "ns2.example.net" } ], "external_id": "crm-domain-1001" } } ] } } ``` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid"] | Создаёт заказ по ранее полученной котировке, а не по произвольной цене. Тот же ключ, покупатель и среда обязательны. Поддерживаются только operation=order/register. Тело одинаково для /orders, /domains и /hosting/orders; последние два маршрута добавляют scopes, но не фильтруют типы строк. Схема: CoreCreateOrderRequest | | `$.quote_uuid` | string | Да | format: "uuid" | UUID неистёкшей, неиспользованной котировки текущего ключа. | | `$.customer_uuid` | string / null | Нет | format: "uuid" | Покупатель котировки; обязателен в managed. | | `$.external_id` | string / null | Нет | maxLength: 255 | ID заказа внешнего биллинга; уникален у реселлера внутри test/live. | #### Регистрация для managed-клиента по ранее выданному quote ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "external_id": "crm-order-1001" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "external_id": "crm-order-1001" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "external_id": "crm-order-1001" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"external_id\": \"crm-order-1001\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"external_id\": \"crm-order-1001\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"external_id\": \"crm-order-1001\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"external_id\": \"crm-order-1001\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Регистрация в aggregate-режиме ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "external_id": "crm-order-1001" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "external_id": "crm-order-1001" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "external_id": "crm-order-1001" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"external_id\": \"crm-order-1001\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"external_id\": \"crm-order-1001\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"external_id\": \"crm-order-1001\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"external_id\": \"crm-order-1001\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 422 | `validation_failed` | Quote истёк/использован/другого окружения; неверная операция, срок, зона, неактивные/чужие контакты; повтор external_id; недостаточно оптового баланса. | Исправьте причину, при необходимости пополните баланс и получите новый quote. Проверьте исходный заказ перед повтором. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 201 Запись создана; проверяйте прикладной status. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Успешный ответ. Схема: CoreOrderEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","customer_uuid","number","status","currency","total_minor","submitted_at","paid_at","completed_at","failed_at","failure_reason","items","invoice","created_at","updated_at"] | Заказ принят не равнозначно домен зарегистрирован. В external checkout выставляется оплаченный счёт и планируется асинхронное исполнение; дальнейшее состояние отслеживайте через заказ/услугу и webhooks. Схема: CoreOrder | | `$.data.uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.external_id` | string / null | Да | | ID заказа внешнего биллинга. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID покупателя. | | `$.data.number` | string | Да | | Номер заказа. | | `$.data.status` | string | Да | enum: "draft", "submitted", "payment_authorized", "registration_processing", "registry_pending", "paid", "active", "completed", "registration_failed", "cancelled", "refunded" | Состояние заказа. submitted: выставлен счёт; paid: оплата зафиксирована; registration_processing/registry_pending: работа с реестром; active/completed: исполнен; registration_failed: регистрация неуспешна. Даты и invoice отражают отдельные стадии, HTTP 201 не терминальный статус. | | `$.data.currency` | string | Да | | Валюта. | | `$.data.total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.submitted_at` | string / null | Да | format: "date-time" | Подан. | | `$.data.paid_at` | string / null | Да | format: "date-time" | Оплачен. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Завершён. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Ошибка. | | `$.data.failure_reason` | string / null | Да | | Причина ошибки; не используйте текст как стабильный машинный код. | | `$.data.items` | array | Да | | Строки. | | `$.data.items[]` | object | Да | required: ["uuid","type","name","quantity","unit_price_minor","total_minor","currency","service_uuid","service_external_id"] | Строка заказа. Схема: CoreOrderItem | | `$.data.items[].uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.items[].type` | string | Да | | Тип услуги, например domain/hosting. | | `$.data.items[].name` | string | Да | | Название. | | `$.data.items[].quantity` | integer | Да | format: "int64" | Количество. | | `$.data.items[].unit_price_minor` | integer | Да | format: "int64" | Цена единицы. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].total_minor` | integer | Да | format: "int64" | Итого. В минимальных единицах валюты, без десятичных дробей. | | `$.data.items[].currency` | string | Да | | Валюта. | | `$.data.items[].service_uuid` | string / null | Да | format: "uuid" | UUID созданной услуги. Отличается от domain UUID. | | `$.data.items[].service_external_id` | string / null | Да | | external_id из payload строки котировки. | | `$.data.invoice` | anyOf | Да | | Счёт или null. | | `$.data.invoice (anyOf 1)` | object | Да | required: ["uuid","number","status","currency","total_minor","paid_minor","payment_collected_by_platform"] | Связанный счёт. Схема: CoreInvoice | | `$.data.invoice (anyOf 1).uuid` | string | Да | format: "uuid" | UUID объекта. | | `$.data.invoice (anyOf 1).number` | string | Да | | Номер. | | `$.data.invoice (anyOf 1).status` | string | Да | | Состояние счёта, например issued/paid/cancelled. | | `$.data.invoice (anyOf 1).currency` | string | Да | | Валюта. | | `$.data.invoice (anyOf 1).total_minor` | integer | Да | format: "int64" | Сумма. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).paid_minor` | integer | Да | format: "int64" | Оплачено. В минимальных единицах валюты, без десятичных дробей. | | `$.data.invoice (anyOf 1).payment_collected_by_platform` | boolean | Да | | false при external checkout: оплату клиента принимает реселлер. paid не означает списание с карты платформой. | | `$.data.invoice (anyOf 2)` | null | Да | | | | `$.data.created_at` | string / null | Да | format: "date-time" | Создан. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Изменён. | #### Заказ при внешнем checkout; сумма только пример, цену берите из quote (201) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000008", "external_id": "crm-order-1001", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "number": "ORD-2026-1001", "status": "paid", "currency": "KZT", "total_minor": 500000, "submitted_at": "2026-10-05T08:00:00+00:00", "paid_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a1234-1000-7000-8000-000000000011", "type": "domain", "name": "example.kz", "quantity": 1, "unit_price_minor": 500000, "total_minor": 500000, "currency": "KZT", "service_uuid": "019a1234-1000-7000-8000-000000000005", "service_external_id": "crm-domain-1001" } ], "invoice": { "uuid": "019a1234-1000-7000-8000-000000000009", "number": "INV-2026-1001", "status": "paid", "currency": "KZT", "total_minor": 500000, "paid_minor": 500000, "payment_collected_by_platform": false }, "created_at": "2026-10-05T08:00:00+00:00", "updated_at": "2026-10-05T08:00:00+00:00" } } ``` #### Заказ с оплатой через платформу; регистрация ещё не выполнена (201) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000008", "external_id": "crm-order-1001", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "number": "ORD-2026-1001", "status": "submitted", "currency": "KZT", "total_minor": 500000, "submitted_at": "2026-10-05T08:00:00+00:00", "paid_at": null, "completed_at": null, "failed_at": null, "failure_reason": null, "items": [ { "uuid": "019a1234-1000-7000-8000-000000000011", "type": "domain", "name": "example.kz", "quantity": 1, "unit_price_minor": 500000, "total_minor": 500000, "currency": "KZT", "service_uuid": "019a1234-1000-7000-8000-000000000005", "service_external_id": "crm-domain-1001" } ], "invoice": { "uuid": "019a1234-1000-7000-8000-000000000009", "number": "INV-2026-1001", "status": "issued", "currency": "KZT", "total_minor": 500000, "paid_minor": 0, "payment_collected_by_platform": true }, "created_at": "2026-10-05T08:00:00+00:00", "updated_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "external_id": "crm-order-1001" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/OrderController.php` ::: --- SOURCE: /reference/getDomain.md # Получить домен `GET /domains/{domainUuid}` **operationId:** `getDomain` Требуемые scopes: domains.read. Локальный снимок, не запрос EPP. Для обновления сначала POST /domains/{domainUuid}/sync и дождитесь completed. Ответ содержит все четыре роли контактов; null означает отсутствие профиля. Массив сырых EPP-статусов и auth-код здесь не раскрываются. После подтверждённого удаления этот endpoint возвращает 404; история операций остаётся доступной. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" curl --request GET "$API_BASE_URL/domains/${DOMAIN_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainEnvelope | | `$.data` | object | Да | required: ["uuid","external_id","service_uuid","customer_uuid","domain_name","unicode_domain_name","punycode_domain_name","status","registration_status","registrar_operation_status","registry_status","verification_status","verification_registry_status","edu_license_status","hold_status","hold_reason","registered_at","expires_at","last_synced_at","zone","contacts","nameservers","created_at","updated_at"] | Схема: DomainRecord | | `$.data.uuid` | string | Да | format: "uuid" | UUID домена; используйте в domainUuid. | | `$.data.external_id` | string / null | Да | | Идентификатор услуги во внешнем биллинге; null для неназначенного. | | `$.data.service_uuid` | string / null | Да | format: "uuid" | UUID услуги для GET /services/{serviceUuid}. | | `$.data.customer_uuid` | string / null | Да | format: "uuid" | UUID плательщика. | | `$.data.domain_name` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.data.unicode_domain_name` | string / null | Да | | Unicode-представление имени. | | `$.data.punycode_domain_name` | string / null | Да | | ASCII/Punycode-представление имени. | | `$.data.status` | string / null | Да | | Локальное состояние услуги: например active, pending, expired. Не статус EPP. | | `$.data.registration_status` | string / null | Да | | Локальное состояние регистрации: например pending, registered, failed. | | `$.data.registrar_operation_status` | string / null | Да | | Последний результат операции регистратора. | | `$.data.registry_status` | string / null | Да | | Сводное состояние реестра; строка не ограничена enum и не является массивом EPP-статусов. | | `$.data.verification_status` | string / null | Да | | Локальное состояние проверки владельца. | | `$.data.verification_registry_status` | string / null | Да | | Состояние проверки владельца в реестре. | | `$.data.edu_license_status` | string / null | Да | | Состояние образовательной лицензии для соответствующих зон. | | `$.data.hold_status` | string / null | Да | | Локальное состояние блокировки. | | `$.data.hold_reason` | string / null | Да | | Причина блокировки; может быть null. | | `$.data.registered_at` | string / null | Да | format: "date-time" | Дата создания в реестре; используется для ограничения продления 24 часа. | | `$.data.expires_at` | string / null | Да | format: "date-time" | Актуальное локальное время окончания; подтверждайте синхронизацией. | | `$.data.last_synced_at` | string / null | Да | format: "date-time" | Последняя синхронизация с реестром. | | `$.data.zone` | anyOf | Да | | Зона или null. | | `$.data.zone (anyOf 1)` | object | Да | required: ["uuid","zone","display_name"] | | | `$.data.zone (anyOf 1).uuid` | string | Да | format: "uuid" | UUID зоны для расчёта quote. | | `$.data.zone (anyOf 1).zone` | string | Да | | Зона без ведущей точки. | | `$.data.zone (anyOf 1).display_name` | string / null | Да | | Отображаемое название зоны. | | `$.data.zone (anyOf 2)` | null | Да | | | | `$.data.contacts` | object | Да | required: ["owner","admin","tech","billing"] | | | `$.data.contacts.owner` | anyOf | Да | | Контакт роли owner или null. | | `$.data.contacts.owner (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data.contacts.owner (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data.contacts.owner (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data.contacts.owner (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data.contacts.owner (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data.contacts.owner (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.contacts.owner (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.contacts.owner (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data.contacts.owner (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data.contacts.owner (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data.contacts.owner (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data.contacts.owner (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data.contacts.owner (anyOf 1).city` | string / null | Да | | Город. | | `$.data.contacts.owner (anyOf 1).region` | string / null | Да | | Регион. | | `$.data.contacts.owner (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.contacts.owner (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.contacts.owner (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data.contacts.owner (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data.contacts.owner (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data.contacts.owner (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data.contacts.owner (anyOf 2)` | null | Да | | | | `$.data.contacts.admin` | anyOf | Да | | Контакт роли admin или null. | | `$.data.contacts.admin (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data.contacts.admin (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data.contacts.admin (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data.contacts.admin (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data.contacts.admin (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data.contacts.admin (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.contacts.admin (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.contacts.admin (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data.contacts.admin (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data.contacts.admin (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data.contacts.admin (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data.contacts.admin (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data.contacts.admin (anyOf 1).city` | string / null | Да | | Город. | | `$.data.contacts.admin (anyOf 1).region` | string / null | Да | | Регион. | | `$.data.contacts.admin (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.contacts.admin (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.contacts.admin (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data.contacts.admin (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data.contacts.admin (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data.contacts.admin (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data.contacts.admin (anyOf 2)` | null | Да | | | | `$.data.contacts.tech` | anyOf | Да | | Контакт роли tech или null. | | `$.data.contacts.tech (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data.contacts.tech (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data.contacts.tech (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data.contacts.tech (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data.contacts.tech (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data.contacts.tech (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.contacts.tech (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.contacts.tech (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data.contacts.tech (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data.contacts.tech (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data.contacts.tech (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data.contacts.tech (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data.contacts.tech (anyOf 1).city` | string / null | Да | | Город. | | `$.data.contacts.tech (anyOf 1).region` | string / null | Да | | Регион. | | `$.data.contacts.tech (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.contacts.tech (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.contacts.tech (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data.contacts.tech (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data.contacts.tech (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data.contacts.tech (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data.contacts.tech (anyOf 2)` | null | Да | | | | `$.data.contacts.billing` | anyOf | Да | | Контакт роли billing или null. | | `$.data.contacts.billing (anyOf 1)` | object | Да | required: ["uuid","type","status","first_name","last_name","middle_name","organization_name","email","phone","country_code","residence_country_code","city","region","address_line","postal_code","external_id_type","verification_status","is_locked_as_registrant","verified_at"] | Схема: DomainContactSnapshot | | `$.data.contacts.billing (anyOf 1).uuid` | string | Да | format: "uuid" | UUID локального профиля контакта, не EPP contact handle. | | `$.data.contacts.billing (anyOf 1).type` | string | Да | enum: "person", "organization" | Тип контакта профиля: person (физическое лицо) или organization (организация). Не путать с типом billing customer. | | `$.data.contacts.billing (anyOf 1).status` | string | Да | | Состояние локального профиля, например active. | | `$.data.contacts.billing (anyOf 1).first_name` | string / null | Да | | Имя. | | `$.data.contacts.billing (anyOf 1).last_name` | string / null | Да | | Фамилия. | | `$.data.contacts.billing (anyOf 1).middle_name` | string / null | Да | | Отчество. | | `$.data.contacts.billing (anyOf 1).organization_name` | string / null | Да | | Название организации. | | `$.data.contacts.billing (anyOf 1).email` | string / null | Да | format: "email" | Email. | | `$.data.contacts.billing (anyOf 1).phone` | string / null | Да | | Телефон. | | `$.data.contacts.billing (anyOf 1).country_code` | string / null | Да | | Код страны адреса. | | `$.data.contacts.billing (anyOf 1).residence_country_code` | string / null | Да | | Код страны резидентства. | | `$.data.contacts.billing (anyOf 1).city` | string / null | Да | | Город. | | `$.data.contacts.billing (anyOf 1).region` | string / null | Да | | Регион. | | `$.data.contacts.billing (anyOf 1).address_line` | string / null | Да | | Адрес. | | `$.data.contacts.billing (anyOf 1).postal_code` | string / null | Да | | Почтовый индекс. | | `$.data.contacts.billing (anyOf 1).external_id_type` | string / null | Да | | Тип документа, например IIN или BIN. Сам идентификатор этот ресурс не раскрывает. | | `$.data.contacts.billing (anyOf 1).verification_status` | string / null | Да | | Состояние проверки контакта. | | `$.data.contacts.billing (anyOf 1).is_locked_as_registrant` | boolean | Да | | Контакт заблокирован как подтверждённый владелец. | | `$.data.contacts.billing (anyOf 1).verified_at` | string / null | Да | format: "date-time" | Время подтверждения или null. | | `$.data.contacts.billing (anyOf 2)` | null | Да | | | | `$.data.nameservers` | array | Да | | Текущие локальные NS; массив может быть пустым. | | `$.data.nameservers[]` | object | Да | required: ["hostname","ipv4","ipv6"] | | | `$.data.nameservers[].hostname` | string | Да | | Имя сервера. | | `$.data.nameservers[].ipv4` | string / null | Да | format: "ipv4" | Glue IPv4. | | `$.data.nameservers[].ipv6` | string / null | Да | format: "ipv6" | Glue IPv6. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создание локальной записи. | | `$.data.updated_at` | string / null | Да | format: "date-time" | Последнее изменение локальной записи. | #### Полная локальная запись зарегистрированного домена (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000001", "external_id": "crm-domain-1001", "service_uuid": "019a1234-1000-7000-8000-000000000005", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "domain_name": "example.kz", "unicode_domain_name": "example.kz", "punycode_domain_name": "example.kz", "status": "active", "registration_status": "registered", "registrar_operation_status": "success", "registry_status": "ok", "verification_status": "pending", "verification_registry_status": null, "edu_license_status": null, "hold_status": "none", "hold_reason": null, "registered_at": "2026-10-01T08:00:00+00:00", "expires_at": "2027-10-01T08:00:00+00:00", "last_synced_at": "2026-10-05T08:00:00+00:00", "zone": { "uuid": "019a1234-1000-7000-8000-000000000004", "zone": "kz", "display_name": ".kz" }, "contacts": { "owner": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null }, "admin": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null }, "tech": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null }, "billing": { "uuid": "019a1234-1000-7000-8000-000000000003", "type": "person", "status": "active", "first_name": "Ivan", "last_name": "Petrov", "middle_name": null, "organization_name": null, "email": "owner@example.net", "phone": "+77000000000", "country_code": "KZ", "residence_country_code": "KZ", "city": "Almaty", "region": "Almaty", "address_line": "Example street 1", "postal_code": "050000", "external_id_type": "IIN", "verification_status": "pending", "is_locked_as_registrant": false, "verified_at": null } }, "nameservers": [ { "hostname": "ns1.example.net", "ipv4": null, "ipv6": null }, { "hostname": "ns2.example.net", "ipv4": null, "ipv6": null } ], "created_at": "2026-10-01T07:59:00+00:00", "updated_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainController.php` ::: --- SOURCE: /reference/deleteDomain.md # Удалить домен у регистратора `DELETE /domains/{domainUuid}` **operationId:** `deleteDomain` Требуемые scopes: domains.delete. Разрушительная операция: запрос удаления в реестр и последующее soft-delete локального домена/услуги после подтверждения. Требуется registered и точное confirm_domain = domain_name. HTTP 202, НИКОГДА не синхронный 204. Подтверждение может означать переход в pendingDelete/redemption, а не немедленную доступность регистрации имени. После completed GET домена может вернуть 404; используйте историю операций. При uncertain повторное удаление не отправляется: выполняется чтение/сверка. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.delete` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["confirm_domain","reason"] | Схема: DomainDeleteRequest | | `$.confirm_domain` | string | Да | maxLength: 253 | Точное значение domain_name из GET домена; сравнение регистрозависимое, без IDN-нормализации. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Удалить домен у регистратора ```json { "confirm_domain": "example.kz", "reason": "Явное поручение владельца удалить домен" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request DELETE "$API_BASE_URL/domains/${DOMAIN_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "confirm_domain": "example.kz", "reason": "Явное поручение владельца удалить домен" }' ``` ```php [PHP 8+] 'DELETE', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "confirm_domain": "example.kz", "reason": "Явное поручение владельца удалить домен" }`; const response = await fetch(url, { method: "DELETE", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"confirm_domain\": \"example.kz\",\n \"reason\": \"Явное поручение владельца удалить домен\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="DELETE") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("DELETE", endpoint, strings.NewReader("{\n \"confirm_domain\": \"example.kz\",\n \"reason\": \"Явное поручение владельца удалить домен\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"confirm_domain\": \"example.kz\",\n \"reason\": \"Явное поручение владельца удалить домен\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("DELETE", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"confirm_domain\": \"example.kz\",\n \"reason\": \"Явное поручение владельца удалить домен\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | confirm_domain не совпадает побайтно с domain_name. | Прочитайте текущий домен и подтвердите точное имя; выполняйте удаление только по явному поручению владельца. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция delete (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "delete", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request DELETE "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "confirm_domain": "example.kz", "reason": "Явное поручение владельца удалить домен" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/checkDomain.md # Проверить доступность имени `POST /domains/check` **operationId:** `checkDomain` Требуемые scopes: domains.read. Синхронный запрос регистратора через маршрут зоны для окружения ключа. Проверка не резервирует имя и не гарантирует последующую регистрацию. HTTP 200 с available=false является нормальным результатом. При replay по прежнему ключу возвращается старая проверка; для новой проверки используйте новый ключ. Не проверяет баланс/контакты/готовность заказа. Отсутствующий маршрут/сбой драйвера может дать 500, а не available=false. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["domain"] | Схема: DomainCheckRequest | | `$.domain` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | #### Проверка демонстрационного имени ```json { "domain": "example.kz" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/check" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "domain": "example.kz" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/check"; const url = baseUrl + path; const body = `{ "domain": "example.kz" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/check" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"domain\": \"example.kz\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/check" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"domain\": \"example.kz\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/check"; URI uri = URI.create(baseUrl + path); String body = "{\n \"domain\": \"example.kz\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/check"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"domain\": \"example.kz\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните доступность API и base URL у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainCheckEnvelope | | `$.data` | object | Да | required: ["domain","available","reason","registrar"] | | | `$.data.domain` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.data.available` | boolean | Да | | Результат проверки в данный момент, не резервирование имени. | | `$.data.reason` | string / null | Да | | Причина из драйвера; значения/язык не фиксированы. | | `$.data.registrar` | string | Да | | Код регистратора выбранного маршрута. | #### Имя доступно на момент проверки; пример не гарантирует реальную доступность (200) ```json { "data": { "domain": "example.kz", "available": true, "reason": null, "registrar": "nic_kz" } } ``` #### Имя занято (200) ```json { "data": { "domain": "example.kz", "available": false, "reason": "Object exists", "registrar": "nic_kz" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/check" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "domain": "example.kz" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainController.php` ::: --- SOURCE: /reference/renewDomain.md # Продлить домен `POST /domains/{domainUuid}/renew` **operationId:** `renewDomain` Требуемые scopes: domains.renew. Quote operation=renew, payload.domain_service_uuid этого домена; period_unit=year, period_count=1..10. Продление возможно не ранее registered_at +24 часа, при известной expires_at; новый срок expires_at + N лет не должен быть позже now +10 календарных лет. Доменные и сервисные статусы должны быть active/expired, registration_status=registered, зона допускает renewal. clientRenewProhibited, serverRenewProhibited, pendingDelete, redemptionPeriod, pendingTransfer запрещают продление. Ещё раз проверяется актуальное состояние реестра перед EPP renew. Нужен provider.checkout_mode=external и действующий одноразовый quote того же API-ключа, окружения, провайдера, клиента и зоны: ровно одна позиция domain_zone с quantity=1. Цену и срок берите из quote, не вычисляйте по цене регистрации. В live резервируется оптовый баланс, после подтверждения резерв списывается; при окончательном отказе освобождается. В test live-баланс не используется. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.renew` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` **x-prerequisite-request-example:** ```json { "description": "Предварительный запрос POST /quotes (другой endpoint, scope quotes.create, отдельный Idempotency-Key). UUID из data.uuid полученного quote передайте в quote_uuid текущей операции. Не используйте здесь демонстрационные UUID или имя без замены.", "method": "POST", "path": "/quotes", "body": { "customer_uuid": "019a1234-1000-7000-8000-000000000002", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a1234-1000-7000-8000-000000000004", "operation": "renew", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a1234-1000-7000-8000-000000000001" } } ] } } ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid","reason"] | Схема: DomainRenewRequest | | `$.quote_uuid` | string | Да | format: "uuid" | UUID неиспользованного, неистёкшего quote operation=renew с одним доменом; срок берётся из quote. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Продлить домен ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "reason": "Клиент продлевает домен на год" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/renew" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "reason": "Клиент продлевает домен на год" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/renew"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "reason": "Клиент продлевает домен на год" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/renew" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"reason\": \"Клиент продлевает домен на год\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/renew" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"reason\": \"Клиент продлевает домен на год\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/renew"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"reason\": \"Клиент продлевает домен на год\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/renew"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"reason\": \"Клиент продлевает домен на год\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 422 | `validation_failed` | Quote не соответствует домену/зоне/операции, истёк/использован; checkout не external; недостаточно средств либо нарушены ограничения срока. | Получите корректный quote/пополните баланс/дождитесь допустимого срока. При неизвестном результате старой операции новую не создавайте. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция renew (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/renew" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "reason": "Клиент продлевает домен на год" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/transferDomain.md # Запросить трансфер существующей локальной записи `POST /domains/{domainUuid}/transfer` **operationId:** `transferDomain` Требуемые scopes: domains.transfer. Для уже существующей локальной записи. Для отсутствующей в биллинге используйте /domains/transfers. Quote operation=transfer с payload.domain_service_uuid, period_unit=year/period_count=1 является тарифом одной операции, а не гарантией увеличения срока на год. auth_code получите у текущего регистратора. Окончание трансфера подтверждается transfer query и domain info, а не HTTP 202. Нужен provider.checkout_mode=external и действующий одноразовый quote того же API-ключа, окружения, провайдера, клиента и зоны: ровно одна позиция domain_zone с quantity=1. Цену и срок берите из quote, не вычисляйте по цене регистрации. В live резервируется оптовый баланс, после подтверждения резерв списывается; при окончательном отказе освобождается. В test live-баланс не используется. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. На стадии обработчика дополнительно проверяются transfer_enabled зоны, hold_status=none и registration_status в registered/pending. Эти проверки могут завершить уже принятую задачу failed; это не обязательно синхронный HTTP 422. **Scopes:** `domains.transfer` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." }, { "status": "failed", "code": "transfer_rejected", "when": "Сверка исходного transfer увидела rejected/canceled.", "recovery": "Резерв освобождается; выясните причину у текущего регистратора." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` **x-prerequisite-request-example:** ```json { "description": "Предварительный запрос POST /quotes (другой endpoint, scope quotes.create, отдельный Idempotency-Key). UUID из data.uuid полученного quote передайте в quote_uuid текущей операции. Не используйте здесь демонстрационные UUID или имя без замены.", "method": "POST", "path": "/quotes", "body": { "customer_uuid": "019a1234-1000-7000-8000-000000000002", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a1234-1000-7000-8000-000000000004", "operation": "transfer", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a1234-1000-7000-8000-000000000001" } } ] } } ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid","auth_code","reason"] | Схема: DomainTransferRequest | | `$.quote_uuid` | string | Да | format: "uuid" | Quote operation=transfer, quantity=1, period_unit=year, period_count=1. | | `$.auth_code` | string | Да | maxLength: 255; Только запись | Код от текущего регистратора; секрет, хранить защищённо. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Запросить трансфер существующей локальной записи ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена по заявке клиента" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/transfer" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена по заявке клиента" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/transfer"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена по заявке клиента" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/transfer" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена по заявке клиента\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/transfer" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена по заявке клиента\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/transfer"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена по заявке клиента\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/transfer"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена по заявке клиента\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 422 | `validation_failed` | Quote не соответствует домену/зоне/операции, истёк/использован; checkout не external; недостаточно средств либо нарушены ограничения срока. | Получите корректный quote/пополните баланс/дождитесь допустимого срока. При неизвестном результате старой операции новую не создавайте. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция transfer (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "transfer", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/transfer" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена по заявке клиента" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/restoreDomain.md # Восстановить домен из периода восстановления `POST /domains/{domainUuid}/restore` **operationId:** `restoreDomain` Требуемые scopes: domains.restore. Доступны принадлежащие провайдеру soft-deleted записи. Зарегистрированный домен вне redemptionPeriod/pendingDelete не допускается. Требуется отдельный quote operation=restore, payload.domain_service_uuid, year/1; используются цены restore, не fallback renewal. Передайте restore_auth или restore_password по требованиям регистратора. Успех подтверждается отсутствием redemptionPeriod/pendingDelete/pendingRestore; это не гарантирует определённое продление срока, смотрите expires_at. Нужен provider.checkout_mode=external и действующий одноразовый quote того же API-ключа, окружения, провайдера, клиента и зоны: ровно одна позиция domain_zone с quantity=1. Цену и срок берите из quote, не вычисляйте по цене регистрации. В live резервируется оптовый баланс, после подтверждения резерв списывается; при окончательном отказе освобождается. В test live-баланс не используется. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.restore` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` **x-prerequisite-request-example:** ```json { "description": "Предварительный запрос POST /quotes (другой endpoint, scope quotes.create, отдельный Idempotency-Key). UUID из data.uuid полученного quote передайте в quote_uuid текущей операции. Не используйте здесь демонстрационные UUID или имя без замены.", "method": "POST", "path": "/quotes", "body": { "customer_uuid": "019a1234-1000-7000-8000-000000000002", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a1234-1000-7000-8000-000000000004", "operation": "restore", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_service_uuid": "019a1234-1000-7000-8000-000000000001" } } ] } } ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid","reason"] | Схема: DomainRestoreRequest | | `$.quote_uuid` | string | Да | format: "uuid" | Отдельный quote operation=restore; не renew. quantity=1, year/1. | | `$.restore_auth` | string / null | Нет | maxLength: 255; Только запись | Данные авторизации восстановления. Нужно непустое restore_auth или restore_password; формат определяется регистратором. | | `$.restore_password` | string / null | Нет | maxLength: 255; Только запись | Альтернативные данные восстановления; требования регистратора могут быть строже. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | | `$ (anyOf 1)` | object | Да | required: ["restore_auth"] | | | `$ (anyOf 1).restore_auth` | string | Да | minLength: 1 | | | `$ (anyOf 2)` | object | Да | required: ["restore_password"] | | | `$ (anyOf 2).restore_password` | string | Да | minLength: 1 | | #### Восстановить домен из периода восстановления ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_auth": "DEMO-RESTORE-AUTH", "reason": "Восстановление по заявке владельца" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/restore" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_auth": "DEMO-RESTORE-AUTH", "reason": "Восстановление по заявке владельца" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/restore"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_auth": "DEMO-RESTORE-AUTH", "reason": "Восстановление по заявке владельца" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/restore" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_auth\": \"DEMO-RESTORE-AUTH\",\n \"reason\": \"Восстановление по заявке владельца\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/restore" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_auth\": \"DEMO-RESTORE-AUTH\",\n \"reason\": \"Восстановление по заявке владельца\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/restore"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_auth\": \"DEMO-RESTORE-AUTH\",\n \"reason\": \"Восстановление по заявке владельца\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/restore"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_auth\": \"DEMO-RESTORE-AUTH\",\n \"reason\": \"Восстановление по заявке владельца\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Восстановление с альтернативным паролем ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_password": "DEMO-RESTORE-PASSWORD", "reason": "Восстановление по заявке владельца" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/restore" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_password": "DEMO-RESTORE-PASSWORD", "reason": "Восстановление по заявке владельца" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/restore"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_password": "DEMO-RESTORE-PASSWORD", "reason": "Восстановление по заявке владельца" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/restore" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_password\": \"DEMO-RESTORE-PASSWORD\",\n \"reason\": \"Восстановление по заявке владельца\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/restore" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_password\": \"DEMO-RESTORE-PASSWORD\",\n \"reason\": \"Восстановление по заявке владельца\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/restore"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_password\": \"DEMO-RESTORE-PASSWORD\",\n \"reason\": \"Восстановление по заявке владельца\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/restore"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"restore_password\": \"DEMO-RESTORE-PASSWORD\",\n \"reason\": \"Восстановление по заявке владельца\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 422 | `validation_failed` | Quote не соответствует домену/зоне/операции, истёк/использован; checkout не external; недостаточно средств либо нарушены ограничения срока. | Получите корректный quote/пополните баланс/дождитесь допустимого срока. При неизвестном результате старой операции новую не создавайте. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция restore (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "restore", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/restore" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "restore_auth": "DEMO-RESTORE-AUTH", "reason": "Восстановление по заявке владельца" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/updateDomainNameservers.md # Заменить делегирование NS `PUT /domains/{domainUuid}/nameservers` **operationId:** `updateDomainNameservers` Требуемые scopes: domains.manage. Полная замена набора NS (2..6), а не добавление одного сервера. Имена нормализуются в Punycode и сравниваются без регистра; дубликаты запрещены. ipv4/ipv6 необязательны и nullable. Это делегирование, не редактирование A/MX/TXT; распространение DNS не входит в срок выполнения задачи. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["nameservers","reason"] | Схема: DomainNameserversRequest | | `$.nameservers` | array | Да | minItems: 2; maxItems: 6 | Полный новый набор NS, не патч; после нормализации имена должны быть различны. | | `$.nameservers[]` | object | Да | required: ["hostname"] | Схема: DomainNameserver | | `$.nameservers[].hostname` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.nameservers[].ipv4` | string / null | Нет | format: "ipv4" | Glue IPv4 или null; не адрес веб-сайта. | | `$.nameservers[].ipv6` | string / null | Нет | format: "ipv6" | Glue IPv6 или null. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Заменить делегирование NS ```json { "nameservers": [ { "hostname": "ns1.example.net", "ipv4": null, "ipv6": null }, { "hostname": "ns2.example.net", "ipv4": null, "ipv6": null } ], "reason": "Смена DNS-провайдера" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/nameservers" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "nameservers": [ { "hostname": "ns1.example.net", "ipv4": null, "ipv6": null }, { "hostname": "ns2.example.net", "ipv4": null, "ipv6": null } ], "reason": "Смена DNS-провайдера" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/nameservers"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "nameservers": [ { "hostname": "ns1.example.net", "ipv4": null, "ipv6": null }, { "hostname": "ns2.example.net", "ipv4": null, "ipv6": null } ], "reason": "Смена DNS-провайдера" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/nameservers" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n },\n {\n \"hostname\": \"ns2.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n }\n ],\n \"reason\": \"Смена DNS-провайдера\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/nameservers" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n },\n {\n \"hostname\": \"ns2.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n }\n ],\n \"reason\": \"Смена DNS-провайдера\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/nameservers"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n },\n {\n \"hostname\": \"ns2.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n }\n ],\n \"reason\": \"Смена DNS-провайдера\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/nameservers"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"nameservers\": [\n {\n \"hostname\": \"ns1.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n },\n {\n \"hostname\": \"ns2.example.net\",\n \"ipv4\": null,\n \"ipv6\": null\n }\n ],\n \"reason\": \"Смена DNS-провайдера\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Список NS не содержит 2..6 элементов, hostname некорректен/повторяется после нормализации либо адрес не соответствует ipv4/ipv6. | Исправьте весь набор NS; повтор с изменённым JSON выполняйте под новым ключом. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция nameservers (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "nameservers", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request PUT "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/nameservers" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "nameservers": [ { "hostname": "ns1.example.net", "ipv4": null, "ipv6": null }, { "hostname": "ns2.example.net", "ipv4": null, "ipv6": null } ], "reason": "Смена DNS-провайдера" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/updateDomainContacts.md # Изменить контакты зарегистрированного домена `PUT /domains/{domainUuid}/contacts` **operationId:** `updateDomainContacts` Требуемые scopes: domains.manage. Передайте все четыре роли контактов; частичная замена не поддержана. UUID должны принадлежать тому же клиенту, провайдеру и окружению. Можно использовать один UUID для всех ролей. Смена владельца и подтверждённого профиля подчиняется дополнительным правилам регистратора; 202 не гарантирует разрешение смены владельца. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. Если локальные и реестровые роли уже совпадают с запросом, менеджер считает это отсутствием изменений и может завершить задачу отказом, а не успешным no-op. Для изменения контактов драйвер должен дополнительно поддерживать DomainInfo; capability contacts отражает только базовый интерфейс. Смена владельца/контактов может сбросить прежнюю проверку и потребовать новой ЭЦП. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["contacts","reason"] | Схема: DomainContactsRequest | | `$.contacts` | object | Да | required: ["owner","admin","tech","billing"]; Дополнительные поля запрещены | | | `$.contacts.owner` | string | Да | format: "uuid" | UUID контакта роли owner того же клиента, провайдера и окружения. Один UUID разрешён во всех ролях. | | `$.contacts.admin` | string | Да | format: "uuid" | UUID контакта роли admin того же клиента, провайдера и окружения. Один UUID разрешён во всех ролях. | | `$.contacts.tech` | string | Да | format: "uuid" | UUID контакта роли tech того же клиента, провайдера и окружения. Один UUID разрешён во всех ролях. | | `$.contacts.billing` | string | Да | format: "uuid" | UUID контакта роли billing того же клиента, провайдера и окружения. Один UUID разрешён во всех ролях. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Изменить контакты зарегистрированного домена ```json { "contacts": { "owner": "019a1234-1000-7000-8000-000000000003", "admin": "019a1234-1000-7000-8000-000000000003", "tech": "019a1234-1000-7000-8000-000000000003", "billing": "019a1234-1000-7000-8000-000000000003" }, "reason": "Актуализация контактов владельца" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/contacts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "contacts": { "owner": "019a1234-1000-7000-8000-000000000003", "admin": "019a1234-1000-7000-8000-000000000003", "tech": "019a1234-1000-7000-8000-000000000003", "billing": "019a1234-1000-7000-8000-000000000003" }, "reason": "Актуализация контактов владельца" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/contacts"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "contacts": { "owner": "019a1234-1000-7000-8000-000000000003", "admin": "019a1234-1000-7000-8000-000000000003", "tech": "019a1234-1000-7000-8000-000000000003", "billing": "019a1234-1000-7000-8000-000000000003" }, "reason": "Актуализация контактов владельца" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/contacts" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"contacts\": {\n \"owner\": \"019a1234-1000-7000-8000-000000000003\",\n \"admin\": \"019a1234-1000-7000-8000-000000000003\",\n \"tech\": \"019a1234-1000-7000-8000-000000000003\",\n \"billing\": \"019a1234-1000-7000-8000-000000000003\"\n },\n \"reason\": \"Актуализация контактов владельца\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/contacts" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"contacts\": {\n \"owner\": \"019a1234-1000-7000-8000-000000000003\",\n \"admin\": \"019a1234-1000-7000-8000-000000000003\",\n \"tech\": \"019a1234-1000-7000-8000-000000000003\",\n \"billing\": \"019a1234-1000-7000-8000-000000000003\"\n },\n \"reason\": \"Актуализация контактов владельца\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/contacts"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"contacts\": {\n \"owner\": \"019a1234-1000-7000-8000-000000000003\",\n \"admin\": \"019a1234-1000-7000-8000-000000000003\",\n \"tech\": \"019a1234-1000-7000-8000-000000000003\",\n \"billing\": \"019a1234-1000-7000-8000-000000000003\"\n },\n \"reason\": \"Актуализация контактов владельца\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/contacts"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"contacts\": {\n \"owner\": \"019a1234-1000-7000-8000-000000000003\",\n \"admin\": \"019a1234-1000-7000-8000-000000000003\",\n \"tech\": \"019a1234-1000-7000-8000-000000000003\",\n \"billing\": \"019a1234-1000-7000-8000-000000000003\"\n },\n \"reason\": \"Актуализация контактов владельца\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Не переданы все четыре роли, переданы посторонние роли или контакт не принадлежит клиенту/провайдеру/окружению. | Создайте/выберите корректные профили и передайте полный набор ролей. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция contacts (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "contacts", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request PUT "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/contacts" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "contacts": { "owner": "019a1234-1000-7000-8000-000000000003", "admin": "019a1234-1000-7000-8000-000000000003", "tech": "019a1234-1000-7000-8000-000000000003", "billing": "019a1234-1000-7000-8000-000000000003" }, "reason": "Актуализация контактов владельца" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/updateDomainStatuses.md # Добавить или снять client-статусы `PUT /domains/{domainUuid}/statuses` **operationId:** `updateDomainStatuses` Требуемые scopes: domains.manage. Разрешены только пять перечисленных client-статусов; server*-статусы управляются реестром. Передайте непустой add или remove; каждый до 5 уникальных значений; одно значение нельзя одновременно добавить и удалить. Снятие статуса не отменяет независимые ограничения реестра. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. Особенность текущей реализации: снятие clientHold идёт как custom-изменение; локальный hold_status не обязательно очищается одновременно. После завершения выполните sync и проверьте состояние; при расхождении обратитесь в поддержку. HTTP 202 не является гарантией снятия всех локальных ограничений. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainStatusesRequest | | `$.add` | array | Нет | maxItems: 5; uniqueItems: true | Добавляемые статусы; уникальны, не пересекаются с remove. | | `$.add[]` | string | Да | enum: "clientTransferProhibited", "clientUpdateProhibited", "clientDeleteProhibited", "clientRenewProhibited", "clientHold" | Разрешённый устанавливаемый client-статус. Статусы server* через этот API не меняются. Схема: DomainClientStatus | | `$.remove` | array | Нет | maxItems: 5; uniqueItems: true | Удаляемые статусы; уникальны. | | `$.remove[]` | string | Да | enum: "clientTransferProhibited", "clientUpdateProhibited", "clientDeleteProhibited", "clientRenewProhibited", "clientHold" | Разрешённый устанавливаемый client-статус. Статусы server* через этот API не меняются. Схема: DomainClientStatus | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | | `$ (anyOf 1)` | object | Да | required: ["add"] | | | `$ (anyOf 1).add` | Условная схема | Да | minItems: 1 | | | `$ (anyOf 2)` | object | Да | required: ["remove"] | | | `$ (anyOf 2).remove` | Условная схема | Да | minItems: 1 | | #### Добавить или снять client-статусы ```json { "add": [ "clientTransferProhibited" ], "reason": "Владелец запрещает трансфер" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/statuses" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "add": [ "clientTransferProhibited" ], "reason": "Владелец запрещает трансфер" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/statuses"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "add": [ "clientTransferProhibited" ], "reason": "Владелец запрещает трансфер" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/statuses" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"add\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец запрещает трансфер\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/statuses" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"add\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец запрещает трансфер\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/statuses"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"add\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец запрещает трансфер\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/statuses"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"add\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец запрещает трансфер\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Снять запрет трансфера ```json { "remove": [ "clientTransferProhibited" ], "reason": "Владелец разрешает перенос" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/statuses" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "remove": [ "clientTransferProhibited" ], "reason": "Владелец разрешает перенос" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/statuses"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "remove": [ "clientTransferProhibited" ], "reason": "Владелец разрешает перенос" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/statuses" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"remove\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец разрешает перенос\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/statuses" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"remove\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец разрешает перенос\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/statuses"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"remove\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец разрешает перенос\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/statuses"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"remove\": [\n \"clientTransferProhibited\"\n ],\n \"reason\": \"Владелец разрешает перенос\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Недопустимый client-статус, дубликаты либо пересечение add/remove. | Разделите добавление/удаление и используйте только разрешённые client-статусы. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция statuses (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "statuses", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request PUT "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/statuses" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "add": [ "clientTransferProhibited" ], "reason": "Владелец запрещает трансфер" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/setDomainWhoisPrivacy.md # Настроить скрытие контактных данных WHOIS `PUT /domains/{domainUuid}/whois-privacy` **operationId:** `setDomainWhoisPrivacy` Требуемые scopes: domains.manage. Используется политика раскрытия контактов регистратора. Поля name/organization/address/phone/fax/email. Применение может быть частичным и перейти в uncertain; это не обещание удалить уже опубликованные данные из чужих WHOIS-кэшей. Текущее локальное состояние читайте capabilities.whois_privacy. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["enabled","reason"] | Схема: DomainPrivacyRequest | | `$.enabled` | boolean | Да | | Включить или выключить скрытие. | | `$.hidden_fields` | array | Нет | uniqueItems: true | Необязательный список скрываемых полей. При пропуске используются настройки менеджера; смотрите capabilities.whois_privacy. | | `$.hidden_fields[]` | string | Да | enum: "name", "organization", "address", "phone", "fax", "email" | Поле раскрытия WHOIS. Схема: DomainPrivacyField | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Настроить скрытие контактных данных WHOIS ```json { "enabled": true, "hidden_fields": [ "email", "phone" ], "reason": "Запрос владельца на конфиденциальность" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/whois-privacy" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "enabled": true, "hidden_fields": [ "email", "phone" ], "reason": "Запрос владельца на конфиденциальность" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/whois-privacy"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "enabled": true, "hidden_fields": [ "email", "phone" ], "reason": "Запрос владельца на конфиденциальность" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/whois-privacy" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"enabled\": true,\n \"hidden_fields\": [\n \"email\",\n \"phone\"\n ],\n \"reason\": \"Запрос владельца на конфиденциальность\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/whois-privacy" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"enabled\": true,\n \"hidden_fields\": [\n \"email\",\n \"phone\"\n ],\n \"reason\": \"Запрос владельца на конфиденциальность\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/whois-privacy"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"enabled\": true,\n \"hidden_fields\": [\n \"email\",\n \"phone\"\n ],\n \"reason\": \"Запрос владельца на конфиденциальность\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/whois-privacy"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"enabled\": true,\n \"hidden_fields\": [\n \"email\",\n \"phone\"\n ],\n \"reason\": \"Запрос владельца на конфиденциальность\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Снять скрытие полей ```json { "enabled": false, "reason": "Владелец разрешает публикацию контактов" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/whois-privacy" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "enabled": false, "reason": "Владелец разрешает публикацию контактов" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/whois-privacy"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "enabled": false, "reason": "Владелец разрешает публикацию контактов" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/whois-privacy" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"enabled\": false,\n \"reason\": \"Владелец разрешает публикацию контактов\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/whois-privacy" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"enabled\": false,\n \"reason\": \"Владелец разрешает публикацию контактов\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/whois-privacy"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"enabled\": false,\n \"reason\": \"Владелец разрешает публикацию контактов\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/whois-privacy"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"enabled\": false,\n \"reason\": \"Владелец разрешает публикацию контактов\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Не передан enabled либо hidden_fields содержит неизвестное/повторяющееся поле. | Используйте boolean enabled и поля из capabilities.whois_privacy.available_fields. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция privacy (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "privacy", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request PUT "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/whois-privacy" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "enabled": true, "hidden_fields": [ "email", "phone" ], "reason": "Запрос владельца на конфиденциальность" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/synchronizeDomain.md # Обновить локальные сведения из реестра `POST /domains/{domainUuid}/sync` **operationId:** `synchronizeDomain` Требуемые scopes: domains.read. Синхронизация ставится в очередь, хотя scope только domains.read; Idempotency-Key и reason обязательны. Разрешена при другой незавершённой задаче; сначала проверяется идентичность домена/регистратора/объекта. После completed повторите GET домена. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainReasonRequest | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Обновить локальные сведения из реестра ```json { "reason": "Проверка актуального срока и состояния" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/sync" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "reason": "Проверка актуального срока и состояния" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/sync"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "reason": "Проверка актуального срока и состояния" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/sync" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"reason\": \"Проверка актуального срока и состояния\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/sync" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"reason\": \"Проверка актуального срока и состояния\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/sync"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"reason\": \"Проверка актуального срока и состояния\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/sync"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"reason\": \"Проверка актуального срока и состояния\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция sync (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "sync", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/sync" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "reason": "Проверка актуального срока и состояния" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/refreshDomainAuthCode.md # Обновить и получить код трансфера `POST /domains/{domainUuid}/auth-code` **operationId:** `refreshDomainAuthCode` Требуемые scopes: domains.auth-code. Операция вызывает refresh auth-кода и может изменить секрет в реестре; это не обычное чтение. В ответе POST код никогда не раскрывается. После completed получите /operations/{operationUuid} с ОБОИМИ scopes domains.read и domains.auth-code. Без дополнительного scope result.auth_code отсутствует даже у завершённой операции. Не выводите токен/код в логах. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.auth-code` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainReasonRequest | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Обновить и получить код трансфера ```json { "reason": "Владелец запросил перенос к другому регистратору" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/auth-code" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "reason": "Владелец запросил перенос к другому регистратору" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/auth-code"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "reason": "Владелец запросил перенос к другому регистратору" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/auth-code" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"reason\": \"Владелец запросил перенос к другому регистратору\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/auth-code" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"reason\": \"Владелец запросил перенос к другому регистратору\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/auth-code"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"reason\": \"Владелец запросил перенос к другому регистратору\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/auth-code"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"reason\": \"Владелец запросил перенос к другому регистратору\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция auth_code (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "auth_code", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/auth-code" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "reason": "Владелец запросил перенос к другому регистратору" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/queryDomainTransfer.md # Запросить состояние текущего трансфера `POST /domains/{domainUuid}/transfer/query` **operationId:** `queryDomainTransfer` Требуемые scopes: domains.transfer. Запрос состояния поставлен в очередь, не непосредственный EPP-ответ. Завершение query-задачи не означает завершение самого трансфера; после completed читайте domain.registry_status. Самостоятельная query не заменяет отслеживание исходной оплаченной transfer-задачи. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. На стадии обработчика дополнительно проверяются transfer_enabled зоны, hold_status=none и registration_status в registered/pending. Эти проверки могут завершить уже принятую задачу failed; это не обязательно синхронный HTTP 422. **Scopes:** `domains.transfer` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainReasonRequest | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Запросить состояние текущего трансфера ```json { "reason": "Уточнение состояния трансфера" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/transfer/query" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "reason": "Уточнение состояния трансфера" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/transfer/query"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "reason": "Уточнение состояния трансфера" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/transfer/query" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"reason\": \"Уточнение состояния трансфера\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/transfer/query" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"reason\": \"Уточнение состояния трансфера\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/transfer/query"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"reason\": \"Уточнение состояния трансфера\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/transfer/query"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"reason\": \"Уточнение состояния трансфера\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция transfer_query (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "transfer_query", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/transfer/query" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "reason": "Уточнение состояния трансфера" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/approveDomainTransfer.md # Подтвердить текущий трансфер `POST /domains/{domainUuid}/transfer/approve` **operationId:** `approveDomainTransfer` Требуемые scopes: domains.transfer. Команда approve текущего трансфера, когда это разрешено текущему регистратору. Не создаёт входящий трансфер и не принимает новый auth_code. Разрешена при незавершённой исходной transfer-задаче; её финансовый результат отслеживайте отдельно. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. На стадии обработчика дополнительно проверяются transfer_enabled зоны, hold_status=none и registration_status в registered/pending. Эти проверки могут завершить уже принятую задачу failed; это не обязательно синхронный HTTP 422. **Scopes:** `domains.transfer` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainReasonRequest | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Подтвердить текущий трансфер ```json { "reason": "Владелец подтверждает исходящий трансфер" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/transfer/approve" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "reason": "Владелец подтверждает исходящий трансфер" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/transfer/approve"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "reason": "Владелец подтверждает исходящий трансфер" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/transfer/approve" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"reason\": \"Владелец подтверждает исходящий трансфер\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/transfer/approve" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"reason\": \"Владелец подтверждает исходящий трансфер\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/transfer/approve"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"reason\": \"Владелец подтверждает исходящий трансфер\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/transfer/approve"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"reason\": \"Владелец подтверждает исходящий трансфер\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция transfer_approve (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "transfer_approve", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/transfer/approve" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "reason": "Владелец подтверждает исходящий трансфер" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/rejectDomainTransfer.md # Отклонить текущий трансфер `POST /domains/{domainUuid}/transfer/reject` **operationId:** `rejectDomainTransfer` Требуемые scopes: domains.transfer. Команда reject; допустимость зависит от роли регистратора и состояния трансфера. Завершение управляющей задачи не эквивалентно немедленному освобождению резерва исходной transfer-задачи; дождитесь её сверки. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. На стадии обработчика дополнительно проверяются transfer_enabled зоны, hold_status=none и registration_status в registered/pending. Эти проверки могут завершить уже принятую задачу failed; это не обязательно синхронный HTTP 422. **Scopes:** `domains.transfer` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainReasonRequest | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Отклонить текущий трансфер ```json { "reason": "Владелец отклоняет несанкционированный перенос" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/transfer/reject" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "reason": "Владелец отклоняет несанкционированный перенос" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/transfer/reject"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "reason": "Владелец отклоняет несанкционированный перенос" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/transfer/reject" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"reason\": \"Владелец отклоняет несанкционированный перенос\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/transfer/reject" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"reason\": \"Владелец отклоняет несанкционированный перенос\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/transfer/reject"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"reason\": \"Владелец отклоняет несанкционированный перенос\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/transfer/reject"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"reason\": \"Владелец отклоняет несанкционированный перенос\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция transfer_reject (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "transfer_reject", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/transfer/reject" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "reason": "Владелец отклоняет несанкционированный перенос" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/cancelDomainTransfer.md # Отменить текущий запрос трансфера `POST /domains/{domainUuid}/transfer/cancel` **operationId:** `cancelDomainTransfer` Требуемые scopes: domains.transfer. Команда cancel существующего трансфера; не отмена произвольной provisioning-задачи. Реестр может запретить отмену. Читайте исходную transfer-задачу и domain.registry_status. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. На стадии обработчика дополнительно проверяются transfer_enabled зоны, hold_status=none и registration_status в registered/pending. Эти проверки могут завершить уже принятую задачу failed; это не обязательно синхронный HTTP 422. **Scopes:** `domains.transfer` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["reason"] | Схема: DomainReasonRequest | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Отменить текущий запрос трансфера ```json { "reason": "Клиент отменил запрос переноса" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/transfer/cancel" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "reason": "Клиент отменил запрос переноса" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/transfer/cancel"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "reason": "Клиент отменил запрос переноса" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/transfer/cancel" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"reason\": \"Клиент отменил запрос переноса\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/transfer/cancel" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"reason\": \"Клиент отменил запрос переноса\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/transfer/cancel"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"reason\": \"Клиент отменил запрос переноса\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/transfer/cancel"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"reason\": \"Клиент отменил запрос переноса\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция transfer_cancel (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "transfer_cancel", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/transfer/cancel" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "reason": "Клиент отменил запрос переноса" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/getDomainHost.md # Прочитать один subordinate host из реестра `GET /domains/{domainUuid}/hosts` **operationId:** `getDomainHost` Требуемые scopes: domains.read. Синхронное чтение одного host по обязательному query hostname; это НЕ список host-объектов. hostname должен находиться внутри текущего домена. Требуются активная интеграция, отсутствие hold и capability host_info. Сырые raw-данные исключены. Обратите внимание: даты createdAt/updatedAt имеют camelCase. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### hostname Расположение: `query`. Обязательный. Полное имя subordinate host; пример ns1.example.kz для домена example.kz. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | **Пример** ```json "ns1.example.kz" ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" curl --request GET "$API_BASE_URL/domains/${DOMAIN_UUID}/hosts?hostname=ns1.example.kz" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/hosts"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path + "?hostname=ns1.example.kz"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/hosts" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path + "?hostname=ns1.example.kz" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/hosts" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path + "?hostname=ns1.example.kz" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/hosts"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path + "?hostname=ns1.example.kz"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/hosts"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?hostname=ns1.example.kz"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция неактивна или домен удерживается для миграции. | Обратитесь к оператору. | | 422 | `validation_failed` | Отсутствует/невалиден hostname, hostname другого домена либо host_info не поддерживается. | Исправьте hostname и проверьте capabilities. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Hostname не является subordinate host текущего домена. | Используйте имя вида ns1.example.kz для домена example.kz и корректные адреса; на чужой домен сменой hostname доступ не получить. | ### HTTP 200 Успешное чтение или сохранение настройки. **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainHostInfoEnvelope | | `$.data` | object | Да | required: ["hostname","exists","status","addresses","statuses","createdAt","updatedAt"] | | | `$.data.hostname` | string | Да | | Имя host-объекта. | | `$.data.exists` | boolean | Да | | Существует ли объект в реестре. | | `$.data.status` | string | Да | | Результат операции драйвера; строка, не закрытый enum. | | `$.data.addresses` | array | Да | | Адреса host-объекта. | | `$.data.addresses[]` | oneOf | Да | | IPv4 или IPv6. | | `$.data.addresses[] (oneOf 1)` | string | Да | format: "ipv4" | | | `$.data.addresses[] (oneOf 2)` | string | Да | format: "ipv6" | | | `$.data.statuses` | array | Да | | Статусы host-объекта. | | `$.data.statuses[]` | string | Да | | Статус EPP без фиксированного enum. | | `$.data.createdAt` | string / null | Да | format: "date-time" | Дата создания в реестре. Именно camelCase. | | `$.data.updatedAt` | string / null | Да | format: "date-time" | Дата изменения в реестре. Именно camelCase. | #### Объект host существует (200) ```json { "data": { "hostname": "ns1.example.kz", "exists": true, "status": "active", "addresses": [ "192.0.2.10", "2001:db8::10" ], "statuses": [ "ok" ], "createdAt": "2026-10-01T08:00:00Z", "updatedAt": null } } ``` #### Host не существует; это не HTTP 404 домена (200) ```json { "data": { "hostname": "ns1.example.kz", "exists": false, "status": "not_found", "addresses": [], "statuses": [], "createdAt": null, "updatedAt": null } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/hosts?hostname=ns1.example.kz" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/createDomainHost.md # Создать subordinate glue-host `POST /domains/{domainUuid}/hosts` **operationId:** `createDomainHost` Требуемые scopes: domains.manage. Разрешён только hostname, оканчивающийся на точку + Punycode текущего домена: ns1.example.kz, не example.kz и не ns1.other.kz. Нужен хотя бы один адрес. Это EPP host-объект (glue), не DNS-запись A/AAAA зоны и не автоматическая смена делегирования. Адреса из примера зарезервированы для документации и должны быть заменены. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["hostname","reason"] | Схема: DomainHostCreateRequest | | `$.hostname` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.ipv4` | string / null | Нет | format: "ipv4" | IPv4 subordinate host; обязателен, если ipv6 отсутствует. | | `$.ipv6` | string / null | Нет | format: "ipv6" | IPv6 subordinate host; обязателен, если ipv4 отсутствует. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | | `$ (anyOf 1)` | object | Да | required: ["ipv4"] | | | `$ (anyOf 1).ipv4` | string | Да | format: "ipv4" | | | `$ (anyOf 2)` | object | Да | required: ["ipv6"] | | | `$ (anyOf 2).ipv6` | string | Да | format: "ipv6" | | #### Создать subordinate glue-host ```json { "hostname": "ns1.example.kz", "ipv4": "192.0.2.10", "ipv6": "2001:db8::10", "reason": "Собственный сервер имён клиента" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/hosts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "hostname": "ns1.example.kz", "ipv4": "192.0.2.10", "ipv6": "2001:db8::10", "reason": "Собственный сервер имён клиента" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/hosts"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "hostname": "ns1.example.kz", "ipv4": "192.0.2.10", "ipv6": "2001:db8::10", "reason": "Собственный сервер имён клиента" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/hosts" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"hostname\": \"ns1.example.kz\",\n \"ipv4\": \"192.0.2.10\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Собственный сервер имён клиента\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/hosts" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"hostname\": \"ns1.example.kz\",\n \"ipv4\": \"192.0.2.10\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Собственный сервер имён клиента\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/hosts"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"hostname\": \"ns1.example.kz\",\n \"ipv4\": \"192.0.2.10\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Собственный сервер имён клиента\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/hosts"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"hostname\": \"ns1.example.kz\",\n \"ipv4\": \"192.0.2.10\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Собственный сервер имён клиента\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Host только с IPv6 ```json { "hostname": "ns1.example.kz", "ipv6": "2001:db8::10", "reason": "Создание IPv6 сервера имён" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/hosts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "hostname": "ns1.example.kz", "ipv6": "2001:db8::10", "reason": "Создание IPv6 сервера имён" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/hosts"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "hostname": "ns1.example.kz", "ipv6": "2001:db8::10", "reason": "Создание IPv6 сервера имён" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/hosts" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"hostname\": \"ns1.example.kz\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Создание IPv6 сервера имён\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/hosts" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"hostname\": \"ns1.example.kz\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Создание IPv6 сервера имён\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/hosts"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"hostname\": \"ns1.example.kz\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Создание IPv6 сервера имён\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/hosts"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"hostname\": \"ns1.example.kz\",\n \"ipv6\": \"2001:db8::10\",\n \"reason\": \"Создание IPv6 сервера имён\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Hostname не является subordinate host текущего домена. Не передан ни один IP либо IP невалиден. | Используйте имя вида ns1.example.kz для домена example.kz и корректные адреса; на чужой домен сменой hostname доступ не получить. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция host_create (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "host_create", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/hosts" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "hostname": "ns1.example.kz", "ipv4": "192.0.2.10", "ipv6": "2001:db8::10", "reason": "Собственный сервер имён клиента" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/updateDomainHost.md # Изменить адреса glue-host `PUT /domains/{domainUuid}/hosts` **operationId:** `updateDomainHost` Требуемые scopes: domains.manage. Меняет набор IP существующего subordinate host. Передайте непустой add_addresses или remove_addresses, каждый до 10; одинаковые нормализованные IP не могут добавляться и удаляться одновременно. Это не замена NS домена и не редактирование DNS-зоны. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["hostname","reason"] | Схема: DomainHostUpdateRequest | | `$.hostname` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.add_addresses` | array | Нет | maxItems: 10; uniqueItems: true | Добавить адреса; не пересекаются с remove_addresses после бинарной нормализации IP. | | `$.add_addresses[]` | oneOf | Да | | IPv4 или IPv6. | | `$.add_addresses[] (oneOf 1)` | string | Да | format: "ipv4" | | | `$.add_addresses[] (oneOf 2)` | string | Да | format: "ipv6" | | | `$.remove_addresses` | array | Нет | maxItems: 10; uniqueItems: true | Удалить адреса. | | `$.remove_addresses[]` | oneOf | Да | | IPv4 или IPv6. | | `$.remove_addresses[] (oneOf 1)` | string | Да | format: "ipv4" | | | `$.remove_addresses[] (oneOf 2)` | string | Да | format: "ipv6" | | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | | `$ (anyOf 1)` | object | Да | required: ["add_addresses"] | | | `$ (anyOf 1).add_addresses` | Условная схема | Да | minItems: 1 | | | `$ (anyOf 2)` | object | Да | required: ["remove_addresses"] | | | `$ (anyOf 2).remove_addresses` | Условная схема | Да | minItems: 1 | | #### Изменить адреса glue-host ```json { "hostname": "ns1.example.kz", "add_addresses": [ "192.0.2.11" ], "remove_addresses": [ "192.0.2.10" ], "reason": "Смена IP сервера имён" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/hosts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "hostname": "ns1.example.kz", "add_addresses": [ "192.0.2.11" ], "remove_addresses": [ "192.0.2.10" ], "reason": "Смена IP сервера имён" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/hosts"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "hostname": "ns1.example.kz", "add_addresses": [ "192.0.2.11" ], "remove_addresses": [ "192.0.2.10" ], "reason": "Смена IP сервера имён" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/hosts" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"hostname\": \"ns1.example.kz\",\n \"add_addresses\": [\n \"192.0.2.11\"\n ],\n \"remove_addresses\": [\n \"192.0.2.10\"\n ],\n \"reason\": \"Смена IP сервера имён\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/hosts" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"hostname\": \"ns1.example.kz\",\n \"add_addresses\": [\n \"192.0.2.11\"\n ],\n \"remove_addresses\": [\n \"192.0.2.10\"\n ],\n \"reason\": \"Смена IP сервера имён\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/hosts"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"hostname\": \"ns1.example.kz\",\n \"add_addresses\": [\n \"192.0.2.11\"\n ],\n \"remove_addresses\": [\n \"192.0.2.10\"\n ],\n \"reason\": \"Смена IP сервера имён\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/hosts"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"hostname\": \"ns1.example.kz\",\n \"add_addresses\": [\n \"192.0.2.11\"\n ],\n \"remove_addresses\": [\n \"192.0.2.10\"\n ],\n \"reason\": \"Смена IP сервера имён\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Hostname не является subordinate host текущего домена. Массивы адресов невалидны, превышают 10 элементов или один IP добавляется и удаляется одновременно. | Используйте имя вида ns1.example.kz для домена example.kz и корректные адреса; на чужой домен сменой hostname доступ не получить. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция host_update (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "host_update", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request PUT "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/hosts" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "hostname": "ns1.example.kz", "add_addresses": [ "192.0.2.11" ], "remove_addresses": [ "192.0.2.10" ], "reason": "Смена IP сервера имён" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/deleteDomainHost.md # Удалить subordinate glue-host `DELETE /domains/{domainUuid}/hosts` **operationId:** `deleteDomainHost` Требуемые scopes: domains.manage. DELETE требует JSON body и возвращает 202, не 204. Разрешён только subordinate host данного домена. Связанный с делегированием host может быть отклонён реестром; сначала измените зависимые делегирования. Удаление host не удаляет домен. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.manage` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." } ] ``` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["hostname","reason"] | Схема: DomainHostDeleteRequest | | `$.hostname` | string | Да | maxLength: 253 | Полное доменное имя без схемы, пути, порта и завершающей точки. IDN допускается; сервер нормализует регистр и преобразует в Punycode UTS46. ASCII-представление не длиннее 253, каждая метка не длиннее 63 символов. example.kz в примерах не означает доступность для регистрации. Схема: DomainName | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Удалить subordinate glue-host ```json { "hostname": "ns1.example.kz", "reason": "Вывод сервера имён из эксплуатации" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request DELETE "$API_BASE_URL/domains/${DOMAIN_UUID}/hosts" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "hostname": "ns1.example.kz", "reason": "Вывод сервера имён из эксплуатации" }' ``` ```php [PHP 8+] 'DELETE', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/hosts"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "hostname": "ns1.example.kz", "reason": "Вывод сервера имён из эксплуатации" }`; const response = await fetch(url, { method: "DELETE", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/hosts" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"hostname\": \"ns1.example.kz\",\n \"reason\": \"Вывод сервера имён из эксплуатации\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="DELETE") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/hosts" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("DELETE", endpoint, strings.NewReader("{\n \"hostname\": \"ns1.example.kz\",\n \"reason\": \"Вывод сервера имён из эксплуатации\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/hosts"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"hostname\": \"ns1.example.kz\",\n \"reason\": \"Вывод сервера имён из эксплуатации\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("DELETE", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/hosts"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"hostname\": \"ns1.example.kz\",\n \"reason\": \"Вывод сервера имён из эксплуатации\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | | 422 | `validation_failed` | Hostname не является subordinate host текущего домена. | Используйте имя вида ns1.example.kz для домена example.kz и корректные адреса; на чужой домен сменой hostname доступ не получить. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция host_delete (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "host_delete", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request DELETE "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/hosts" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "hostname": "ns1.example.kz", "reason": "Вывод сервера имён из эксплуатации" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/transferDomainIn.md # Перенести домен, которого ещё нет в биллинге `POST /domains/transfers` **operationId:** `transferDomainIn` Требуемые scopes: domains.transfer. Создаёт pending-услугу и локальный домен, затем одну оплачиваемую transfer-задачу. Предварительно получите quote operation=transfer, quantity=1, year/1; payload.domain_name и четыре contact_uuids обязательны, external_id необязателен. Контакты одного клиента/провайдера/окружения; регламент зоны применяется к владельцу. Для managed обязателен customer_uuid; aggregate использует назначенного клиента. Поддерживается только маршрут transfer_enabled с активным credential нужного окружения. Наличие такого домена у любого провайдера в том же окружении или незавершённое приобретение вызывает 409, не автоматическое присвоение. Для имеющейся записи используйте /domains/{domainUuid}/transfer. Дополнительные scopes до этого шага: contacts.write и quotes.create; для последующего опроса domains.read. Тариф одной операции transfer не гарантирует +1 год, итоговая expires_at берётся из реестра. Нужен provider.checkout_mode=external и действующий одноразовый quote того же API-ключа, окружения, провайдера, клиента и зоны: ровно одна позиция domain_zone с quantity=1. Цену и срок берите из quote, не вычисляйте по цене регистрации. В live резервируется оптовый баланс, после подтверждения резерв списывается; при окончательном отказе освобождается. В test live-баланс не используется. HTTP 202 подтверждает только сохранение задачи, не успешную EPP-команду. Сохраните data.uuid; проверяйте GET /operations/{operationUuid} (domains.read) либо события domain.operation.updated. pending/processing не окончательны; completed подтверждает выполнение, failed означает окончательный отказ. При uncertain не создавайте новую операцию: сверка запланирована, смотрите next_check_at (обычно +5 минут). Для результата, который нельзя доказать чтением/журналом, потребуется поддержка. Зарезервированные средства удерживаются до подтверждения или окончательного отказа. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.transfer` **x-prerequisite-scopes:** ```json [ { "scope": "quotes.create", "when": "Для выпуска quote до вызова этого endpoint." }, { "scope": "contacts.write", "when": "Если контактные профили ещё не созданы." }, { "scope": "domains.read", "when": "Для последующего чтения домена/операции." } ] ``` **x-async-error-cases:** ```json [ { "status": "failed", "code": "validation_failed", "when": "Обработчик отклонил входные данные/состояние либо получил отказ, классифицированный как ValidationException.", "recovery": "Изучите ограничения и журнал через поддержку; новый запрос только после окончательного отказа и устранения причины." }, { "status": "failed", "code": "registry_rejected", "when": "Есть относящийся к этой задаче подтверждённый отказ реестра либо ошибка до отправки.", "recovery": "Не повторяйте неизменённую команду; исправьте причину. Резерв освобождается." }, { "status": "uncertain", "code": "registry_result_unknown", "when": "После начала действия нет надёжного подтверждения, произошёл timeout, идентичность ответа не совпала либо сверка пока не установила результат.", "recovery": "Не посылайте вторую мутацию. Опрос исходного UUID после next_check_at; при длительном ожидании передайте UUID и request_id поддержке. Резерв не освобождается автоматически." }, { "status": "failed", "code": "transfer_rejected", "when": "Сверка исходного transfer увидела rejected/canceled.", "recovery": "Резерв освобождается; выясните причину у текущего регистратора." } ] ``` **x-prerequisite-request-example:** ```json { "description": "Предварительный запрос POST /quotes (другой endpoint, scope quotes.create, отдельный Idempotency-Key). UUID из data.uuid полученного quote передайте в quote_uuid текущей операции. Не используйте здесь демонстрационные UUID или имя без замены.", "method": "POST", "path": "/quotes", "body": { "customer_uuid": "019a1234-1000-7000-8000-000000000002", "items": [ { "resource_type": "domain_zone", "resource_uuid": "019a1234-1000-7000-8000-000000000004", "operation": "transfer", "period_unit": "year", "period_count": 1, "quantity": 1, "currency": "KZT", "payload": { "domain_name": "example.kz", "contact_uuids": { "owner": "019a1234-1000-7000-8000-000000000003", "admin": "019a1234-1000-7000-8000-000000000003", "tech": "019a1234-1000-7000-8000-000000000003", "billing": "019a1234-1000-7000-8000-000000000003" }, "external_id": "crm-domain-1001" } } ] } } ``` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["quote_uuid","auth_code","reason"] | Схема: DomainIncomingTransferRequest | | `$.quote_uuid` | string | Да | format: "uuid" | Quote operation=transfer, quantity=1, period_unit=year, period_count=1. | | `$.auth_code` | string | Да | maxLength: 255; Только запись | Код от текущего регистратора; секрет, хранить защищённо. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | | `$.customer_uuid` | string / null | Нет | format: "uuid" | В managed обязательно передайте своего клиента; в aggregate не передавайте или укажите назначенного aggregate-клиента. | #### Перенос для managed-клиента ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена клиента в наш биллинг" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/transfers" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена клиента в наш биллинг" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/transfers"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена клиента в наш биллинг" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/transfers" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена клиента в наш биллинг\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/transfers" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена клиента в наш биллинг\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/transfers"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена клиента в наш биллинг\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/transfers"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"customer_uuid\": \"019a1234-1000-7000-8000-000000000002\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос домена клиента в наш биллинг\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Перенос для aggregate-клиента ```json { "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос агрегированной услуги реселлера" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/transfers" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос агрегированной услуги реселлера" }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/transfers"; const url = baseUrl + path; const body = `{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос агрегированной услуги реселлера" }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/transfers" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос агрегированной услуги реселлера\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/transfers" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос агрегированной услуги реселлера\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/transfers"; URI uri = URI.create(baseUrl + path); String body = "{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос агрегированной услуги реселлера\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/transfers"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"quote_uuid\": \"019a1234-1000-7000-8000-000000000006\",\n \"auth_code\": \"DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE\",\n \"reason\": \"Перенос агрегированной услуги реселлера\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция домена неактивна, migration hold или есть pending/processing/uncertain/awaiting_registry задача. | Дождитесь исходной задачи либо обратитесь к оператору. sync и управление текущим трансфером разрешены при занятой очереди. | | 422 | `validation_failed` | Драйвер не поддерживает действие либо домен не зарегистрирован для требующего регистрации действия. | Проверьте capabilities и состояние домена; не вызывайте неподдерживаемое действие. | | 409 | `conflict` | Домен уже есть в окружении, приобретение выполняется или external_id услуги занят. | Найдите существующую запись; не создавайте второй домен. При несовпадении владельца обратитесь к оператору. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 202 Задача принята; результат реестра проверяется отдельно. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принята операция transfer (202) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "transfer", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/transfers" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "quote_uuid": "019a1234-1000-7000-8000-000000000006", "customer_uuid": "019a1234-1000-7000-8000-000000000002", "auth_code": "DEMO-REPLACE-WITH-CURRENT-REGISTRAR-CODE", "reason": "Перенос домена клиента в наш биллинг" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/getDomainCapabilities.md # Проверить поддерживаемые операции `GET /domains/{domainUuid}/capabilities` **operationId:** `getDomainCapabilities` Требуемые scopes: domains.read. Ответ зависит от интерфейсов драйвера, active integration credential и migration hold, но НЕ учитывает scopes ключа, баланс и все EPP-ограничения. true не обещает успешное действие. DNSSEC всегда false; методов управления DNSSEC или A/MX/TXT здесь нет. renewal_available_at отражает только первые 24 часа, maximum_term_years ограничивает оставшийся срок; дополнительные запреты смотрите в описании renew. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" curl --request GET "$API_BASE_URL/domains/${DOMAIN_UUID}/capabilities" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/capabilities"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/capabilities" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/capabilities" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/capabilities"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/capabilities"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainCapabilitiesEnvelope | | `$.data` | object | Да | required: ["operations","renewal_available_at","maximum_term_years","auto_renew","whois_privacy"] | | | `$.data.operations` | object | Да | required: ["renew","transfer","transfer_query","transfer_approve","transfer_reject","transfer_cancel","restore","nameservers","contacts","statuses","delete","auth_code","privacy","sync","host_create","host_update","host_delete","host_info","dnssec"] | | | `$.data.operations.renew` | boolean | Да | | Поддержка renew драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.transfer` | boolean | Да | | Поддержка transfer драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.transfer_query` | boolean | Да | | Поддержка transfer_query драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.transfer_approve` | boolean | Да | | Поддержка transfer_approve драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.transfer_reject` | boolean | Да | | Поддержка transfer_reject драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.transfer_cancel` | boolean | Да | | Поддержка transfer_cancel драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.restore` | boolean | Да | | Поддержка restore драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.nameservers` | boolean | Да | | Поддержка nameservers драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.contacts` | boolean | Да | | Поддержка contacts драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.statuses` | boolean | Да | | Поддержка statuses драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.delete` | boolean | Да | | Поддержка delete драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.auth_code` | boolean | Да | | Поддержка auth_code драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.privacy` | boolean | Да | | Поддержка privacy драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.sync` | boolean | Да | | Поддержка sync драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.host_create` | boolean | Да | | Поддержка host_create драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.host_update` | boolean | Да | | Поддержка host_update драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.host_delete` | boolean | Да | | Поддержка host_delete драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.host_info` | boolean | Да | | Поддержка host_info драйвером при активной интеграции и отсутствии migration hold. Не гарантирует достаточный scope/баланс/допустимость текущего состояния. | | `$.data.operations.dnssec` | boolean | Да | | Всегда false: DNSSEC не реализован в текущем контракте. | | `$.data.renewal_available_at` | string / null | Да | format: "date-time" | registered_at + 24 часа; null, если дата регистрации неизвестна. Дополнительные ограничения продления сохраняются. | | `$.data.maximum_term_years` | integer | Да | const: 10 | Предельный суммарный оставшийся срок: новая expires_at не позднее текущего времени + 10 календарных лет. | | `$.data.auto_renew` | boolean | Да | | Согласие на автопродление. | | `$.data.whois_privacy` | object | Да | required: ["enabled","status","hidden_fields","available_fields","last_applied_at"] | | | `$.data.whois_privacy.enabled` | boolean | Да | | Локальное включение WHOIS privacy. | | `$.data.whois_privacy.status` | string | Да | | Состояние применения; строка без закрытого enum. | | `$.data.whois_privacy.hidden_fields` | array | Да | | Скрываемые поля. | | `$.data.whois_privacy.hidden_fields[]` | string | Да | enum: "name", "organization", "address", "phone", "fax", "email" | Поле раскрытия WHOIS. Схема: DomainPrivacyField | | `$.data.whois_privacy.available_fields` | array | Да | | Поддерживаемые поля политики раскрытия. | | `$.data.whois_privacy.available_fields[]` | object | Да | required: ["code","label","registrar_field","description"] | | | `$.data.whois_privacy.available_fields[].code` | string | Да | enum: "name", "organization", "address", "phone", "fax", "email" | Поле раскрытия WHOIS. Схема: DomainPrivacyField | | `$.data.whois_privacy.available_fields[].label` | string | Да | | Локализованное название. | | `$.data.whois_privacy.available_fields[].registrar_field` | string | Да | | XML-поле регистратора. | | `$.data.whois_privacy.available_fields[].description` | string | Да | | Локализованное пояснение. | | `$.data.whois_privacy.last_applied_at` | string / null | Да | format: "date-time" | Последнее применение либо null. | #### Пример возможностей настроенного драйвера; проверяйте ответ конкретного домена (200) ```json { "data": { "operations": { "renew": true, "transfer": true, "transfer_query": true, "transfer_approve": true, "transfer_reject": true, "transfer_cancel": true, "restore": true, "nameservers": true, "contacts": true, "statuses": true, "delete": true, "auth_code": true, "privacy": true, "sync": true, "host_create": true, "host_update": true, "host_delete": true, "host_info": true, "dnssec": false }, "renewal_available_at": "2026-10-02T08:00:00+00:00", "maximum_term_years": 10, "auto_renew": false, "whois_privacy": { "enabled": false, "status": "disabled", "hidden_fields": [], "available_fields": [ { "code": "name", "label": "Имя", "registrar_field": "contact:name", "description": "Настройка раскрытия поля Имя" }, { "code": "organization", "label": "Организация", "registrar_field": "contact:org", "description": "Настройка раскрытия поля Организация" }, { "code": "address", "label": "Адрес", "registrar_field": "contact:addr", "description": "Настройка раскрытия поля Адрес" }, { "code": "phone", "label": "Телефон", "registrar_field": "contact:voice", "description": "Настройка раскрытия поля Телефон" }, { "code": "fax", "label": "Факс", "registrar_field": "contact:fax", "description": "Настройка раскрытия поля Факс" }, { "code": "email", "label": "Email", "registrar_field": "contact:email", "description": "Настройка раскрытия поля Email" } ], "last_applied_at": null } } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/capabilities" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/setDomainAutoRenew.md # Включить или выключить автопродление `PUT /domains/{domainUuid}/auto-renew` **operationId:** `setDomainAutoRenew` Требуемые scopes: domains.manage. Синхронное сохранение настройки: HTTP 200, не задача EPP. При enabled=true ДОПОЛНИТЕЛЬНО нужен domains.renew. Только external checkout; активная интеграция и отсутствие migration hold. Согласие относится к текущему ключу. Планировщик рассматривает домен ежечасно в окне 7 дней до окончания и создаёт продление на 1 год по АКТУАЛЬНОМУ тарифу; цена не фиксируется в момент включения. Применяются ограничения 24 часа/10 лет, баланс и реестровые запреты. Отозванный ключ, отсутствующая цена или нехватка средств не гарантируют продление; следите за domain.auto_renew.failed и domain.operation.updated. Отключение не отменяет уже принятую задачу. Retail auto-invoices не включаются; test не использует live-средства. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.manage` **Условные права доступа:** - `domains.renew`: enabled=true **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["enabled","reason"] | Схема: DomainAutoRenewRequest | | `$.enabled` | boolean | Да | | Согласие на автопродление на 1 год по актуальному тарифу; false отключает. | | `$.reason` | string | Да | minLength: 3; maxLength: 1000 | Причина действия для журнала аудита; не передавайте пароли или персональные данные. Схема: DomainReason | #### Согласие на актуальную цену ```json { "enabled": true, "reason": "Клиент согласовал продление по текущему тарифу" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/auto-renew" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "enabled": true, "reason": "Клиент согласовал продление по текущему тарифу" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/auto-renew"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "enabled": true, "reason": "Клиент согласовал продление по текущему тарифу" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/auto-renew" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"enabled\": true,\n \"reason\": \"Клиент согласовал продление по текущему тарифу\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/auto-renew" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"enabled\": true,\n \"reason\": \"Клиент согласовал продление по текущему тарифу\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/auto-renew"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"enabled\": true,\n \"reason\": \"Клиент согласовал продление по текущему тарифу\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/auto-renew"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"enabled\": true,\n \"reason\": \"Клиент согласовал продление по текущему тарифу\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### Отключение будущих продлений ```json { "enabled": false, "reason": "Клиент отключил автоматическое продление" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PUT "$API_BASE_URL/domains/${DOMAIN_UUID}/auto-renew" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "enabled": false, "reason": "Клиент отключил автоматическое продление" }' ``` ```php [PHP 8+] 'PUT', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/auto-renew"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "enabled": false, "reason": "Клиент отключил автоматическое продление" }`; const response = await fetch(url, { method: "PUT", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/auto-renew" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"enabled\": false,\n \"reason\": \"Клиент отключил автоматическое продление\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PUT") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/auto-renew" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PUT", endpoint, strings.NewReader("{\n \"enabled\": false,\n \"reason\": \"Клиент отключил автоматическое продление\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/auto-renew"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"enabled\": false,\n \"reason\": \"Клиент отключил автоматическое продление\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PUT", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/auto-renew"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PUT"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"enabled\": false,\n \"reason\": \"Клиент отключил автоматическое продление\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 403 | `forbidden` | enabled=true без domains.renew. | Добавьте domains.renew либо отправьте enabled=false. | | 409 | `conflict` | Неактивная интеграция или migration hold. | Устраните ограничение через оператора. | | 422 | `validation_failed` | Checkout провайдера не external. | Эта настройка недоступна в platform checkout. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainAutoRenewEnvelope | | `$.data` | object | Да | required: ["domain_uuid","auto_renew","period_years","days_before_expiry"] | | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.auto_renew` | boolean | Да | | Сохранённое согласие. | | `$.data.period_years` | integer | Да | const: 1 | Срок автоматического продления. | | `$.data.days_before_expiry` | integer | Да | const: 7 | Окно, в котором планировщик рассматривает продление. | #### Согласие сохранено (200) ```json { "data": { "domain_uuid": "019a1234-1000-7000-8000-000000000001", "auto_renew": true, "period_years": 1, "days_before_expiry": 7 } } ``` #### Будущие автопродления выключены (200) ```json { "data": { "domain_uuid": "019a1234-1000-7000-8000-000000000001", "auto_renew": false, "period_years": 1, "days_before_expiry": 7 } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request PUT "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/auto-renew" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "enabled": true, "reason": "Клиент согласовал продление по текущему тарифу" }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/getDomainOperation.md # Прочитать состояние доменной операции `GET /operations/{operationUuid}` **operationId:** `getDomainOperation` Требуемые scopes: domains.read. Только reseller_domain_operation своего провайдера и окружения. register_domain здесь отсутствует: регистрацию отслеживайте через Order/Service/Domain. HTTP 200 возможен при любом прикладном статусе, включая failed и uncertain. result обычно [] до completed. result.auth_code раскрывается только для GET при domains.auth-code; без него поле отсутствует. Не кэшируйте/не логируйте секреты. Polling с backoff/jitter, учитывайте next_check_at, а не каждую секунду. failed окончателен; ошибку анализируйте в data.error, не как HTTP ErrorEnvelope. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` **Условные права доступа:** - `domains.auth-code`: Для чтения result.auth_code. **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### operationUuid Расположение: `path`. Обязательный. UUID из ответа доменной операции, не UUID заказа или register_domain. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Пример** ```json "019a1234-1000-7000-8000-000000000007" ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" OPERATION_UUID='019a1234-1000-7000-8000-000000000007' curl --request GET "$API_BASE_URL/operations/${OPERATION_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/operations/{operationUuid}"; path = path.replaceAll("{operationUuid}", encodeURIComponent((process.env.OPERATION_UUID || "019a1234-1000-7000-8000-000000000007"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/operations/{operationUuid}" path = path.replace("{operationUuid}", quote(os.environ.get("OPERATION_UUID", "019a1234-1000-7000-8000-000000000007"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/operations/{operationUuid}" value0 := os.Getenv("OPERATION_UUID") if value0 == "" { value0 = "019a1234-1000-7000-8000-000000000007" } path = strings.ReplaceAll(path, "{operationUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/operations/{operationUuid}"; path = path.replace("{operationUuid}", URLEncoder.encode(envOr("OPERATION_UUID", "019a1234-1000-7000-8000-000000000007"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/operations/{operationUuid}"; path = path.Replace("{operationUuid}", Uri.EscapeDataString((Environment.GetEnvironmentVariable("OPERATION_UUID") ?? "019a1234-1000-7000-8000-000000000007"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Заголовок `Cache-Control`**: Запрет хранения ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "no-store" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainOperationEnvelope | | `$.data` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data.uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data.domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data.action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data.status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data.result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data.result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data.result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data.result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data.result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data.result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data.error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data.error (anyOf 1)` | null | Да | | | | `$.data.error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data.error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data.error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data.created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data.completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data.next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | #### Принято, EPP ещё не подтверждён (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "pending", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:00:00+00:00" } } ``` #### Обработчик выполняет запрос (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "processing", "result": [], "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": null } } ``` #### Подтверждённое продление (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "completed", "result": { "domain_uuid": "019a1234-1000-7000-8000-000000000001", "expires_at": "2028-10-01T08:00:00+00:00" }, "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": "2026-10-05T08:00:03+00:00", "next_check_at": null } } ``` #### Окончательный отказ; проверьте ограничения, резерв освобождён (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "failed", "result": [], "error": { "code": "registry_rejected", "message": "Operation was rejected. Check domain data and registrar restrictions." }, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": null } } ``` #### Результат неизвестен; новую команду отправлять нельзя (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "uncertain", "result": [], "error": { "code": "registry_result_unknown", "message": "Registry result is not confirmed. Do not repeat this operation; reconciliation is scheduled." }, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": "2026-10-05T08:05:00+00:00" } } ``` #### GET с дополнительным domains.auth-code; код демонстрационный (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "auth_code", "status": "completed", "result": { "domain_uuid": "019a1234-1000-7000-8000-000000000001", "expires_at": "2027-10-01T08:00:00+00:00", "auth_code": "DEMO-NOT-A-REAL-AUTH-CODE" }, "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": "2026-10-05T08:00:03+00:00", "next_check_at": null } } ``` #### GET без domains.auth-code: поле auth_code отсутствует (200) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "auth_code", "status": "completed", "result": { "domain_uuid": "019a1234-1000-7000-8000-000000000001", "expires_at": "2027-10-01T08:00:00+00:00" }, "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": "2026-10-05T08:00:03+00:00", "next_check_at": null } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/operations/019a1234-1000-7000-8000-000000000007" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/listDomainOperations.md # История доменных операций `GET /domains/{domainUuid}/operations` **operationId:** `listDomainOperations` Требуемые scopes: domains.read. Пагинация id по убыванию, только reseller_domain_operation; без задач регистрации/клиентского renewal. Доступна для soft-deleted доменов того же провайдера/окружения. Ответ отличается от /domains: только data и meta.next_cursor, без links/path/prev_cursor. При дополнительном domains.auth-code результаты могут содержать auth-коды; исключите тело из логирования. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.read` **Условные права доступа:** - `domains.auth-code`: Для чтения result.auth_code. **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" curl --request GET "$API_BASE_URL/domains/${DOMAIN_UUID}/operations?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/operations"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/operations" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/operations" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/operations"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/operations"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Заголовок `Cache-Control`**: Запрет хранения ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "no-store" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Схема: DomainOperationListEnvelope | | `$.data` | array | Да | | Только reseller_domain_operation; register_domain сюда не входит. | | `$.data[]` | object | Да | required: ["uuid","domain_uuid","action","status","result","error","created_at","completed_at","next_check_at"] | Схема: DomainOperation | | `$.data[].uuid` | string | Да | format: "uuid" | UUID задачи для опроса /operations/{operationUuid}. | | `$.data[].domain_uuid` | string | Да | format: "uuid" | UUID связанного домена. | | `$.data[].action` | string | Да | enum: "renew", "transfer", "restore", "nameservers", "contacts", "statuses", "privacy", "sync", "auth_code", "transfer_query", "transfer_approve", "transfer_reject", "transfer_cancel", "host_create", "host_update", "host_delete", "delete" | Тип операции приложения, не команда EPP. | | `$.data[].status` | string | Да | | Состояние очереди. pending: ожидает; processing: выполняется; completed: подтверждена; failed: окончательный отказ; uncertain: результат не подтверждён, выполняется сверка. Не трактовать неизвестное значение как успех. | | `$.data[].result` | oneOf | Да | | Итог не включает сырой EPP-ответ, полную запись домена или HTTP-код реестра. Схема: DomainOperationResult | | `$.data[].result (oneOf 1)` | array | Да | maxItems: 0 | До завершения result сериализуется как [], а не {}. | | `$.data[].result (oneOf 2)` | object | Да | required: ["domain_uuid","expires_at"] | | | `$.data[].result (oneOf 2).domain_uuid` | string | Да | format: "uuid" | UUID домена. | | `$.data[].result (oneOf 2).expires_at` | string / null | Да | format: "date-time" | Срок домена после подтверждения; null допустим. | | `$.data[].result (oneOf 2).auth_code` | string | Нет | | Секрет. Только GET, action=auth_code и дополнительный scope domains.auth-code; иначе поле отсутствует. Не логировать. | | `$.data[].error` | anyOf | Да | | Ошибка операции; это не HTTP ErrorEnvelope. | | `$.data[].error (anyOf 1)` | null | Да | | | | `$.data[].error (anyOf 2)` | object | Да | required: ["code","message"] | | | `$.data[].error (anyOf 2).code` | string | Да | | Машинный код обработки: registry_rejected, validation_failed, transfer_rejected или registry_result_unknown; новые значения допустимы. | | `$.data[].error (anyOf 2).message` | string | Да | | Безопасное сообщение; не используйте текст как машинный код. | | `$.data[].created_at` | string / null | Да | format: "date-time" | Время постановки. | | `$.data[].completed_at` | string / null | Да | format: "date-time" | Заполняется при completed; при failed остаётся null. | | `$.data[].next_check_at` | string / null | Да | format: "date-time" | Для pending/uncertain: не опрашивать чаще указанного времени; это не SLA. Для прочих статусов null. | | `$.meta` | object | Да | required: ["next_cursor"] | | | `$.meta.next_cursor` | string / null | Да | | Следующая страница или null. | #### История доменных операций (200) ```json { "data": [ { "uuid": "019a1234-1000-7000-8000-000000000007", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "completed", "result": { "domain_uuid": "019a1234-1000-7000-8000-000000000001", "expires_at": "2028-10-01T08:00:00+00:00" }, "error": null, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": "2026-10-05T08:00:03+00:00", "next_check_at": null }, { "uuid": "019a1234-1000-7000-8000-000000000012", "domain_uuid": "019a1234-1000-7000-8000-000000000001", "action": "renew", "status": "failed", "result": [], "error": { "code": "registry_rejected", "message": "Operation was rejected. Check domain data and registrar restrictions." }, "created_at": "2026-10-05T08:00:00+00:00", "completed_at": null, "next_check_at": null } ], "meta": { "next_cursor": null } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/operations" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainOperationController.php` ::: --- SOURCE: /reference/getRegistrantVerificationPayload.md # Получить точный документ для ЭЦП владельца `GET /domains/{domainUuid}/registrant-verification/payload` **operationId:** `getRegistrantVerificationPayload` Требуемые scopes: domains.verify. Домен должен быть создан в реестре; нужны активная интеграция и отсутствие migration hold. Если последняя принятая подпись pending или владелец уже verified, новая подготовка блокируется 422. Данные владельца могут запрашиваться у NIC.KZ. Подписывайте ТОЧНЫЕ UTF-8 байты signable_payload через NCALayer с возвращёнными параметрами; не собирайте документ самостоятельно и не подписывайте пример. payload_hash = SHA-256 текста. expires_at = issued_at+15 минут рекомендует обновить документ; submit не принимает отдельный подписанный token срока, а проверяет актуальный snapshot и точный текст. Ответ содержит идентификатор владельца, не сохраняйте его в публичных логах. Это не общая доменная Operation. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.verify` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" curl --request GET "$API_BASE_URL/domains/${DOMAIN_UUID}/registrant-verification/payload" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/registrant-verification/payload"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/registrant-verification/payload" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/registrant-verification/payload" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/registrant-verification/payload"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/registrant-verification/payload"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция неактивна или migration hold. | Обратитесь к оператору. | | 422 | `validation_failed` | Домен не создан в реестре, последняя проверка pending или уже verified. | Дождитесь завершения регистрации/верификации; повторная подпись не нужна. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 200 Успешное чтение или сохранение настройки. **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainVerificationPayloadEnvelope | | `$.data` | object | Да | required: ["type","eds_provider","doc_spec_alg","signer_role","payload_hash","signable_payload","payload_snapshot","issued_at","expires_at","ncalayer_options","signature_options"] | | | `$.data.type` | string | Да | const: "eds" | Поддерживаемый способ. | | `$.data.eds_provider` | string | Да | const: "ncalayer" | Провайдер подписи. | | `$.data.doc_spec_alg` | string | Да | const: "DOC-SPEC-REGISTRANT-IDENTITY-CONFIRMATION-V1" | Версия подписываемого документа. | | `$.data.signer_role` | string | Да | const: "CURRENT_REGISTRANT" | Текущий владелец домена. | | `$.data.payload_hash` | string | Да | pattern: "^[a-f0-9]{64}$" | SHA-256 точной строки signable_payload. | | `$.data.signable_payload` | string | Да | | Полный двуязычный документ. Подписывайте без изменения пробелов, переносов, кодировки UTF-8 и порядка строк. | | `$.data.payload_snapshot` | object | Да | required: ["domain-name","domain-creation-time","registrant-name","registrant-org","registrant-residencedetails-country","registrant-residencedetails-externalidtype","registrant-residencedetails-externalidvalue"] | Схема: DomainVerificationSnapshot | | `$.data.payload_snapshot.domain-name` | string | Да | | Точное имя из реестра, Punycode. | | `$.data.payload_snapshot.domain-creation-time` | string / null | Да | | Дата создания в EPP, сохраняйте исходную строку. | | `$.data.payload_snapshot.registrant-name` | string / null | Да | | Имя владельца из реестра/манифеста. | | `$.data.payload_snapshot.registrant-org` | string / null | Да | | Организация владельца либо null. | | `$.data.payload_snapshot.registrant-residencedetails-country` | string / null | Да | | Страна резидентства. | | `$.data.payload_snapshot.registrant-residencedetails-externalidtype` | string / null | Да | | Тип документа, например IIN/BIN. | | `$.data.payload_snapshot.registrant-residencedetails-externalidvalue` | string / null | Да | | Чувствительный идентификатор владельца; не логируйте. | | `$.data.issued_at` | string | Да | format: "date-time" | Время формирования. | | `$.data.expires_at` | string | Да | format: "date-time" | Рекомендуемое время обновления документа (issued_at + 15 минут). При submit сервер сверяет актуальные данные и точный текст, но отдельный timestamp/token срока не принимает. | | `$.data.ncalayer_options` | object | Да | required: ["environment","allowedStorages","locale","signerParams"] | | | `$.data.ncalayer_options.environment` | string | Да | | Окружение NCALayer, test или production. | | `$.data.ncalayer_options.allowedStorages` | anyOf | Да | | Разрешённые хранилища или null в production. | | `$.data.ncalayer_options.allowedStorages (anyOf 1)` | array | Да | | В test [PKCS12]. | | `$.data.ncalayer_options.allowedStorages (anyOf 1)[]` | string | Да | | Идентификатор хранилища. | | `$.data.ncalayer_options.allowedStorages (anyOf 2)` | null | Да | | | | `$.data.ncalayer_options.locale` | string | Да | | Локаль NCALayer. | | `$.data.ncalayer_options.signerParams` | object | Да | required: ["extKeyUsageOids","chain"] | | | `$.data.ncalayer_options.signerParams.extKeyUsageOids` | array | Да | | Ограничения назначения сертификата. | | `$.data.ncalayer_options.signerParams.extKeyUsageOids[]` | string | Да | | OID назначения ключа. | | `$.data.ncalayer_options.signerParams.chain` | anyOf | Да | | Цепочка сертификатов или null. | | `$.data.ncalayer_options.signerParams.chain (anyOf 1)` | array | Да | | Тестовая цепочка. | | `$.data.ncalayer_options.signerParams.chain (anyOf 1)[]` | string | Да | | Сертификат цепочки. | | `$.data.ncalayer_options.signerParams.chain (anyOf 2)` | null | Да | | | | `$.data.signature_options` | object | Да | required: ["method","format","decode","encapsulate","digested","timestamp_applied","cms_type","cades_profile"] | Схема: DomainSignatureOptions | | `$.data.signature_options.method` | string | Да | maxLength: 80 | Для действительной подписи строго kz.gov.pki.knca.basics.sign. | | `$.data.signature_options.format` | string | Да | maxLength: 20 | Для действительной подписи строго cms. | | `$.data.signature_options.decode` | boolean | Да | const: false | Подписываются исходные текстовые байты, не Base64-декодированный документ. | | `$.data.signature_options.encapsulate` | boolean | Да | const: false | Только detached CMS. | | `$.data.signature_options.digested` | boolean | Да | const: false | Не передавать предварительный дайджест вместо документа. | | `$.data.signature_options.timestamp_applied` | boolean | Да | const: true | Нужна метка времени CAdES-T. | | `$.data.signature_options.cms_type` | string | Да | maxLength: 80 | Для действительной подписи строго CMS Detached. | | `$.data.signature_options.cades_profile` | string | Да | maxLength: 80 | Для действительной подписи строго CAdES-T. | #### Полный документ и SHA-256; данные демонстрационные, не подписывать этот пример (200) ```json { "data": { "type": "eds", "eds_provider": "ncalayer", "doc_spec_alg": "DOC-SPEC-REGISTRANT-IDENTITY-CONFIRMATION-V1", "signer_role": "CURRENT_REGISTRANT", "payload_hash": "4f7ec7ab0a228267838add37968eb6f14cce085679ece865136db71afe11fb51", "signable_payload": "[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\n\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\n\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\n---------------------------------------------------------------------------\n* Домендік атау: example.kz\n* Құрылған күні: 2026-10-01T08:00:00.000Z\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\n* Ұйым: \n* Ел: KZ\n* Құжат түрі: IIN\n* Құжат нөмірі: 000000000000\n\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\n\nKazNIC. Барлық құқықтар қорғалған.\n\n\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\n\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\nказахстанского сегмента Интернета.\n\nДАННЫЕ ОПЕРАЦИИ:\n---------------------------------------------------------------------------\n* Доменное имя: example.kz\n* Дата создания: 2026-10-01T08:00:00.000Z\n* Регистрант (ФИО): Ivan Petrov\n* Организация: \n* Страна: KZ\n* Тип документа: IIN\n* Номер документа: 000000000000\n\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\n\nKazNIC. Все права защищены.", "payload_snapshot": { "domain-name": "example.kz", "domain-creation-time": "2026-10-01T08:00:00.000Z", "registrant-name": "Ivan Petrov", "registrant-org": null, "registrant-residencedetails-country": "KZ", "registrant-residencedetails-externalidtype": "IIN", "registrant-residencedetails-externalidvalue": "000000000000" }, "issued_at": "2026-10-05T08:00:00+00:00", "expires_at": "2026-10-05T08:15:00+00:00", "ncalayer_options": { "environment": "production", "allowedStorages": null, "locale": "ru", "signerParams": { "extKeyUsageOids": [ "1.3.6.1.5.5.7.3.4" ], "chain": null } }, "signature_options": { "method": "kz.gov.pki.knca.basics.sign", "format": "cms", "decode": false, "encapsulate": false, "digested": false, "timestamp_applied": true, "cms_type": "CMS Detached", "cades_profile": "CAdES-T" } } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request GET "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/registrant-verification/payload" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainController.php` ::: --- SOURCE: /reference/submitRegistrantVerification.md # Отправить ЭЦП и создать попытку проверки владельца `POST /domains/{domainUuid}/registrant-verification` **operationId:** `submitRegistrantVerification` Требуемые scopes: domains.verify. Сначала получите свежий payload. Передайте payload_snapshot, точный signable_payload и signature_options без изменений, signature = Base64 DER detached CMS CAdES-T с меткой времени. Проверяются криптография, доверие, отзыв и соответствие ИИН/БИН/полномочий владельцу; специальная политика ИП для .edu.kz не отменяет криптопроверку. Невалидный тип подписи, CMS, mismatch документа или отказ регистратора может создать Verification со status=failed и HTTP 201, а не HTTP 422. HTTP 422 означает ошибку HTTP-валидации либо запрет новой попытки (pending/already verified/конкурентная обработка). Проверяйте data.status и failure_message; отдельный failure_code ресурс не выдаёт. Для pending ждите реестр, новую подпись не отправляйте; если ожидание затянулось, обращайтесь в поддержку. Отказ по образовательной лицензии независим от действительности ЭЦП. JSON-пример полный, но содержит демонстрационный идентификатор и НЕ настоящую подпись; подпись всегда формируется на устройстве владельца по свежему документу. Обязателен Idempotency-Key (1..160 символов). Повтор с тем же ключом должен сохранять метод, путь, query и точные байты JSON. HTTP replay обычно хранится 24 часа и возвращает первоначальный ответ, а не актуальную задачу; проверяйте GET /operations/{operationUuid}. Ключ доменной задачи нельзя переиспользовать после истечения HTTP TTL. Лимит общий для всех запросов одного API-ключа (rate_limit_per_minute, значение по умолчанию при выдаче ключа 120, индивидуальное значение может отличаться). Окно счётчика 60 секунд. Учитывайте X-RateLimit-Limit и X-RateLimit-Remaining; при 429 дождитесь Retry-After секунд и добавьте jitter. Polling и повторы также расходуют лимит; лимиты реестра независимы. **Scopes:** `domains.verify` **x-input-caveats:** ```json [ "Передавайте синтаксически корректный UUID. Не все методы выполняют явную UUID-валидацию до SQL; поведение malformed UUID не унифицировано и в PostgreSQL может дать 500 вместо 404/422. Это ограничение реализации, не рекомендуемый сценарий клиента." ] ``` ## Параметры ### domainUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. JSON body; Content-Type: application/json. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["type","payload_snapshot","signable_payload"] | Схема: DomainVerificationRequest | | `$.type` | string | Да | maxLength: 80 | На уровне HTTP принимается строка до 80; фактически поддерживается только eds. Другой тип создаёт failed Verification. | | `$.payload_snapshot` | object | Да | required: ["domain-name","domain-creation-time","registrant-name","registrant-org","registrant-residencedetails-country","registrant-residencedetails-externalidtype","registrant-residencedetails-externalidvalue"] | Сервер HTTP проверяет array; успешная бизнес-проверка требует точные семь ключей из GET. Не добавляйте/не удаляйте поля. Схема: DomainVerificationSnapshot | | `$.payload_snapshot.domain-name` | string | Да | | Точное имя из реестра, Punycode. | | `$.payload_snapshot.domain-creation-time` | string / null | Да | | Дата создания в EPP, сохраняйте исходную строку. | | `$.payload_snapshot.registrant-name` | string / null | Да | | Имя владельца из реестра/манифеста. | | `$.payload_snapshot.registrant-org` | string / null | Да | | Организация владельца либо null. | | `$.payload_snapshot.registrant-residencedetails-country` | string / null | Да | | Страна резидентства. | | `$.payload_snapshot.registrant-residencedetails-externalidtype` | string / null | Да | | Тип документа, например IIN/BIN. | | `$.payload_snapshot.registrant-residencedetails-externalidvalue` | string / null | Да | | Чувствительный идентификатор владельца; не логируйте. | | `$.signable_payload` | string | Да | | Скопируйте полный signable_payload из свежего GET без изменений. | | `$.signature` | string / null | Нет | Только запись | Base64 DER detached CMS CAdES-T, обязательна при type=eds. Пример ниже не является действительной подписью. | | `$.certificate` | string / null | Нет | Только запись | Необязательный сертификат подписанта. | | `$.eds_provider` | string / null | Нет | maxLength: 80 | Идентификатор провайдера; рекомендуется ncalayer. | | `$.signature_options` | object | Нет | | HTTP-валидатор допускает неполный объект/nullable-поля. Для действительной EDS-проверки передайте ВСЕ восемь значений из signature_options GET; иначе возможен HTTP 201 со status=failed. Схема: DomainSignatureRequestOptions | | `$.signature_options.method` | string / null | Нет | maxLength: 80 | Для действительной подписи строго kz.gov.pki.knca.basics.sign. | | `$.signature_options.format` | string / null | Нет | maxLength: 20 | Для действительной подписи строго cms. | | `$.signature_options.decode` | boolean / null | Нет | | Подписываются исходные текстовые байты, не Base64-декодированный документ. | | `$.signature_options.encapsulate` | boolean / null | Нет | | Только detached CMS. | | `$.signature_options.digested` | boolean / null | Нет | | Не передавать предварительный дайджест вместо документа. | | `$.signature_options.timestamp_applied` | boolean / null | Нет | | Нужна метка времени CAdES-T. | | `$.signature_options.cms_type` | string / null | Нет | maxLength: 80 | Для действительной подписи строго CMS Detached. | | `$.signature_options.cades_profile` | string / null | Нет | maxLength: 80 | Для действительной подписи строго CAdES-T. | | `$ (allOf 1)` | Условная схема | Да | | | | `$ (allOf 1) (if)` | object | Да | required: ["type"] | | | `$ (allOf 1) (if).type` | Условная схема | Да | const: "eds" | | | `$ (allOf 1) (then)` | object | Да | required: ["signature","signature_options"] | | | `$ (allOf 1) (then).signature` | string | Да | minLength: 1 | | | `$ (allOf 1) (then).signature_options` | object | Да | minProperties: 1 | | #### Структура отправки; замените signature результатом подписи документа из GET ```json { "type": "eds", "payload_snapshot": { "domain-name": "example.kz", "domain-creation-time": "2026-10-01T08:00:00.000Z", "registrant-name": "Ivan Petrov", "registrant-org": null, "registrant-residencedetails-country": "KZ", "registrant-residencedetails-externalidtype": "IIN", "registrant-residencedetails-externalidvalue": "000000000000" }, "signable_payload": "[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\n\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\n\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\n---------------------------------------------------------------------------\n* Домендік атау: example.kz\n* Құрылған күні: 2026-10-01T08:00:00.000Z\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\n* Ұйым: \n* Ел: KZ\n* Құжат түрі: IIN\n* Құжат нөмірі: 000000000000\n\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\n\nKazNIC. Барлық құқықтар қорғалған.\n\n\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\n\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\nказахстанского сегмента Интернета.\n\nДАННЫЕ ОПЕРАЦИИ:\n---------------------------------------------------------------------------\n* Доменное имя: example.kz\n* Дата создания: 2026-10-01T08:00:00.000Z\n* Регистрант (ФИО): Ivan Petrov\n* Организация: \n* Страна: KZ\n* Тип документа: IIN\n* Номер документа: 000000000000\n\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\n\nKazNIC. Все права защищены.", "signature": "REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER", "certificate": null, "eds_provider": "ncalayer", "signature_options": { "method": "kz.gov.pki.knca.basics.sign", "format": "cms", "decode": false, "encapsulate": false, "digested": false, "timestamp_applied": true, "cms_type": "CMS Detached", "cades_profile": "CAdES-T" } } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${DOMAIN_UUID:?Set DOMAIN_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/domains/${DOMAIN_UUID}/registrant-verification" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "type": "eds", "payload_snapshot": { "domain-name": "example.kz", "domain-creation-time": "2026-10-01T08:00:00.000Z", "registrant-name": "Ivan Petrov", "registrant-org": null, "registrant-residencedetails-country": "KZ", "registrant-residencedetails-externalidtype": "IIN", "registrant-residencedetails-externalidvalue": "000000000000" }, "signable_payload": "[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\n\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\n\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\n---------------------------------------------------------------------------\n* Домендік атау: example.kz\n* Құрылған күні: 2026-10-01T08:00:00.000Z\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\n* Ұйым: \n* Ел: KZ\n* Құжат түрі: IIN\n* Құжат нөмірі: 000000000000\n\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\n\nKazNIC. Барлық құқықтар қорғалған.\n\n\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\n\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\nказахстанского сегмента Интернета.\n\nДАННЫЕ ОПЕРАЦИИ:\n---------------------------------------------------------------------------\n* Доменное имя: example.kz\n* Дата создания: 2026-10-01T08:00:00.000Z\n* Регистрант (ФИО): Ivan Petrov\n* Организация: \n* Страна: KZ\n* Тип документа: IIN\n* Номер документа: 000000000000\n\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\n\nKazNIC. Все права защищены.", "signature": "REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER", "certificate": null, "eds_provider": "ncalayer", "signature_options": { "method": "kz.gov.pki.knca.basics.sign", "format": "cms", "decode": false, "encapsulate": false, "digested": false, "timestamp_applied": true, "cms_type": "CMS Detached", "cades_profile": "CAdES-T" } }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/domains/{domainUuid}/registrant-verification"; path = path.replaceAll("{domainUuid}", encodeURIComponent(requiredEnv("DOMAIN_UUID"))); const url = baseUrl + path; const body = `{ "type": "eds", "payload_snapshot": { "domain-name": "example.kz", "domain-creation-time": "2026-10-01T08:00:00.000Z", "registrant-name": "Ivan Petrov", "registrant-org": null, "registrant-residencedetails-country": "KZ", "registrant-residencedetails-externalidtype": "IIN", "registrant-residencedetails-externalidvalue": "000000000000" }, "signable_payload": "[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\\n\\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\\n\\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\\n---------------------------------------------------------------------------\\n* Домендік атау: example.kz\\n* Құрылған күні: 2026-10-01T08:00:00.000Z\\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\\n* Ұйым: \\n* Ел: KZ\\n* Құжат түрі: IIN\\n* Құжат нөмірі: 000000000000\\n\\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\\n\\nKazNIC. Барлық құқықтар қорғалған.\\n\\n\\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\\n\\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\\nказахстанского сегмента Интернета.\\n\\nДАННЫЕ ОПЕРАЦИИ:\\n---------------------------------------------------------------------------\\n* Доменное имя: example.kz\\n* Дата создания: 2026-10-01T08:00:00.000Z\\n* Регистрант (ФИО): Ivan Petrov\\n* Организация: \\n* Страна: KZ\\n* Тип документа: IIN\\n* Номер документа: 000000000000\\n\\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\\n\\nKazNIC. Все права защищены.", "signature": "REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER", "certificate": null, "eds_provider": "ncalayer", "signature_options": { "method": "kz.gov.pki.knca.basics.sign", "format": "cms", "decode": false, "encapsulate": false, "digested": false, "timestamp_applied": true, "cms_type": "CMS Detached", "cades_profile": "CAdES-T" } }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/domains/{domainUuid}/registrant-verification" path = path.replace("{domainUuid}", quote(required_env("DOMAIN_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"type\": \"eds\",\n \"payload_snapshot\": {\n \"domain-name\": \"example.kz\",\n \"domain-creation-time\": \"2026-10-01T08:00:00.000Z\",\n \"registrant-name\": \"Ivan Petrov\",\n \"registrant-org\": null,\n \"registrant-residencedetails-country\": \"KZ\",\n \"registrant-residencedetails-externalidtype\": \"IIN\",\n \"registrant-residencedetails-externalidvalue\": \"000000000000\"\n },\n \"signable_payload\": \"[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\\n\\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\\n\\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\\n---------------------------------------------------------------------------\\n* Домендік атау: example.kz\\n* Құрылған күні: 2026-10-01T08:00:00.000Z\\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\\n* Ұйым: \\n* Ел: KZ\\n* Құжат түрі: IIN\\n* Құжат нөмірі: 000000000000\\n\\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\\n\\nKazNIC. Барлық құқықтар қорғалған.\\n\\n\\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\\n\\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\\nказахстанского сегмента Интернета.\\n\\nДАННЫЕ ОПЕРАЦИИ:\\n---------------------------------------------------------------------------\\n* Доменное имя: example.kz\\n* Дата создания: 2026-10-01T08:00:00.000Z\\n* Регистрант (ФИО): Ivan Petrov\\n* Организация: \\n* Страна: KZ\\n* Тип документа: IIN\\n* Номер документа: 000000000000\\n\\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\\n\\nKazNIC. Все права защищены.\",\n \"signature\": \"REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER\",\n \"certificate\": null,\n \"eds_provider\": \"ncalayer\",\n \"signature_options\": {\n \"method\": \"kz.gov.pki.knca.basics.sign\",\n \"format\": \"cms\",\n \"decode\": false,\n \"encapsulate\": false,\n \"digested\": false,\n \"timestamp_applied\": true,\n \"cms_type\": \"CMS Detached\",\n \"cades_profile\": \"CAdES-T\"\n }\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/domains/{domainUuid}/registrant-verification" value0 := requiredEnv("DOMAIN_UUID") path = strings.ReplaceAll(path, "{domainUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"type\": \"eds\",\n \"payload_snapshot\": {\n \"domain-name\": \"example.kz\",\n \"domain-creation-time\": \"2026-10-01T08:00:00.000Z\",\n \"registrant-name\": \"Ivan Petrov\",\n \"registrant-org\": null,\n \"registrant-residencedetails-country\": \"KZ\",\n \"registrant-residencedetails-externalidtype\": \"IIN\",\n \"registrant-residencedetails-externalidvalue\": \"000000000000\"\n },\n \"signable_payload\": \"[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\\n\\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\\n\\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\\n---------------------------------------------------------------------------\\n* Домендік атау: example.kz\\n* Құрылған күні: 2026-10-01T08:00:00.000Z\\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\\n* Ұйым: \\n* Ел: KZ\\n* Құжат түрі: IIN\\n* Құжат нөмірі: 000000000000\\n\\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\\n\\nKazNIC. Барлық құқықтар қорғалған.\\n\\n\\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\\n\\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\\nказахстанского сегмента Интернета.\\n\\nДАННЫЕ ОПЕРАЦИИ:\\n---------------------------------------------------------------------------\\n* Доменное имя: example.kz\\n* Дата создания: 2026-10-01T08:00:00.000Z\\n* Регистрант (ФИО): Ivan Petrov\\n* Организация: \\n* Страна: KZ\\n* Тип документа: IIN\\n* Номер документа: 000000000000\\n\\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\\n\\nKazNIC. Все права защищены.\",\n \"signature\": \"REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER\",\n \"certificate\": null,\n \"eds_provider\": \"ncalayer\",\n \"signature_options\": {\n \"method\": \"kz.gov.pki.knca.basics.sign\",\n \"format\": \"cms\",\n \"decode\": false,\n \"encapsulate\": false,\n \"digested\": false,\n \"timestamp_applied\": true,\n \"cms_type\": \"CMS Detached\",\n \"cades_profile\": \"CAdES-T\"\n }\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/domains/{domainUuid}/registrant-verification"; path = path.replace("{domainUuid}", URLEncoder.encode(requiredEnv("DOMAIN_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"type\": \"eds\",\n \"payload_snapshot\": {\n \"domain-name\": \"example.kz\",\n \"domain-creation-time\": \"2026-10-01T08:00:00.000Z\",\n \"registrant-name\": \"Ivan Petrov\",\n \"registrant-org\": null,\n \"registrant-residencedetails-country\": \"KZ\",\n \"registrant-residencedetails-externalidtype\": \"IIN\",\n \"registrant-residencedetails-externalidvalue\": \"000000000000\"\n },\n \"signable_payload\": \"[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\\n\\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\\n\\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\\n---------------------------------------------------------------------------\\n* Домендік атау: example.kz\\n* Құрылған күні: 2026-10-01T08:00:00.000Z\\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\\n* Ұйым: \\n* Ел: KZ\\n* Құжат түрі: IIN\\n* Құжат нөмірі: 000000000000\\n\\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\\n\\nKazNIC. Барлық құқықтар қорғалған.\\n\\n\\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\\n\\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\\nказахстанского сегмента Интернета.\\n\\nДАННЫЕ ОПЕРАЦИИ:\\n---------------------------------------------------------------------------\\n* Доменное имя: example.kz\\n* Дата создания: 2026-10-01T08:00:00.000Z\\n* Регистрант (ФИО): Ivan Petrov\\n* Организация: \\n* Страна: KZ\\n* Тип документа: IIN\\n* Номер документа: 000000000000\\n\\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\\n\\nKazNIC. Все права защищены.\",\n \"signature\": \"REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER\",\n \"certificate\": null,\n \"eds_provider\": \"ncalayer\",\n \"signature_options\": {\n \"method\": \"kz.gov.pki.knca.basics.sign\",\n \"format\": \"cms\",\n \"decode\": false,\n \"encapsulate\": false,\n \"digested\": false,\n \"timestamp_applied\": true,\n \"cms_type\": \"CMS Detached\",\n \"cades_profile\": \"CAdES-T\"\n }\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/domains/{domainUuid}/registrant-verification"; path = path.Replace("{domainUuid}", Uri.EscapeDataString(RequiredEnv("DOMAIN_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"type\": \"eds\",\n \"payload_snapshot\": {\n \"domain-name\": \"example.kz\",\n \"domain-creation-time\": \"2026-10-01T08:00:00.000Z\",\n \"registrant-name\": \"Ivan Petrov\",\n \"registrant-org\": null,\n \"registrant-residencedetails-country\": \"KZ\",\n \"registrant-residencedetails-externalidtype\": \"IIN\",\n \"registrant-residencedetails-externalidvalue\": \"000000000000\"\n },\n \"signable_payload\": \"[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\\n\\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\\n\\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\\n---------------------------------------------------------------------------\\n* Домендік атау: example.kz\\n* Құрылған күні: 2026-10-01T08:00:00.000Z\\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\\n* Ұйым: \\n* Ел: KZ\\n* Құжат түрі: IIN\\n* Құжат нөмірі: 000000000000\\n\\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\\n\\nKazNIC. Барлық құқықтар қорғалған.\\n\\n\\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\\n\\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\\nказахстанского сегмента Интернета.\\n\\nДАННЫЕ ОПЕРАЦИИ:\\n---------------------------------------------------------------------------\\n* Доменное имя: example.kz\\n* Дата создания: 2026-10-01T08:00:00.000Z\\n* Регистрант (ФИО): Ivan Petrov\\n* Организация: \\n* Страна: KZ\\n* Тип документа: IIN\\n* Номер документа: 000000000000\\n\\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\\n\\nKazNIC. Все права защищены.\",\n \"signature\": \"REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER\",\n \"certificate\": null,\n \"eds_provider\": \"ncalayer\",\n \"signature_options\": {\n \"method\": \"kz.gov.pki.knca.basics.sign\",\n \"format\": \"cms\",\n \"decode\": false,\n \"encapsulate\": false,\n \"digested\": false,\n \"timestamp_applied\": true,\n \"cms_type\": \"CMS Detached\",\n \"cades_profile\": \"CAdES-T\"\n }\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет действительного Bearer API-ключа. | Проверьте ключ, срок действия и окружение; не повторяйте бесконечно. | | 403 | `forbidden` | Нет требуемого scope, провайдер/ключ/доступ запрещён. | Проверьте настройки провайдера и scopes ключа. | | 429 | `rate_limit_exceeded` | Исчерпан общий лимит ключа. | Выждите Retry-After; повторы выполняйте с backoff и jitter. | | 500 | `internal_error` | Необработанный сбой инфраструктуры или внешнего драйвера. | Сохраните request_id. Для изменения сначала проверьте существующую задачу; не генерируйте новый ключ вслепую. Формат ответа reverse proxy может отличаться от API. | | 404 | `resource_not_found` | UUID не найден у этого провайдера/окружения, либо запись удалена/скрыта migration hold. | Проверьте UUID, test/live и принадлежность. Не пытайтесь получить ресурс чужого провайдера. | | 409 | `conflict` | Интеграция неактивна или migration hold. | Обратитесь к оператору. | | 422 | `validation_failed` | Не хватает полей; type=eds без signature/options; уже pending/verified или конкурентная обработка. | Исправьте формат либо дождитесь текущей попытки. Новый Idempotency-Key не снимает блокировку pending. | | 409 | `conflict` | Ключ уже используется другим запросом или исходный запрос ещё выполняется. | Не изменяйте тело при повторе. Проверьте исходную операцию; используйте новый ключ только для нового намеренного действия. | | 422 | `validation_failed` | Невалидный JSON-пayload/поле или отсутствует Idempotency-Key. | Исправьте данные по error.details; для изменённого запроса создайте новый ключ. | | 404 | `resource_not_found` | Reseller API глобально выключен конфигурацией. | Уточните base URL и включение API у оператора. | ### HTTP 201 Запись создана; проверяйте прикладной status. **Заголовок `Idempotency-Replayed`**: Присутствует со значением true при возврате сохранённого HTTP-ответа. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Схема: DomainVerificationEnvelope | | `$.data` | object | Да | required: ["uuid","type","status","eds_provider","verification_status","verified_at","failed_at","expires_at","failure_message"] | | | `$.data.uuid` | string | Да | format: "uuid" | UUID попытки проверки. | | `$.data.type` | string | Да | | Способ проверки, например eds. | | `$.data.status` | string | Да | | Состояние попытки; как минимум pending, verified, failed. HTTP 201 не означает verified. | | `$.data.eds_provider` | string / null | Да | | Провайдер подписи. | | `$.data.verification_status` | string | Да | | Дублирует status этой попытки. | | `$.data.verified_at` | string / null | Да | format: "date-time" | Время успешной проверки или null. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Время отказа или null. | | `$.data.expires_at` | string / null | Да | format: "date-time" | Срок действия подтверждения либо null; не путать со сроком домена/документа. | | `$.data.failure_message` | string / null | Да | | Локализованная причина отказа; отдельный failure_code в этом ресурсе не выдаётся. | #### Подпись принята, ожидается реестр (201) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000010", "type": "eds", "status": "pending", "eds_provider": "ncalayer", "verification_status": "pending", "verified_at": null, "failed_at": null, "expires_at": null, "failure_message": null } } ``` #### Владелец подтверждён (201) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000010", "type": "eds", "status": "verified", "eds_provider": "ncalayer", "verification_status": "verified", "verified_at": "2026-10-05T08:00:05+00:00", "failed_at": null, "expires_at": null, "failure_message": null } } ``` #### HTTP 201: попытка создана, но подпись отклонена (201) ```json { "data": { "uuid": "019a1234-1000-7000-8000-000000000010", "type": "eds", "status": "failed", "eds_provider": "ncalayer", "verification_status": "failed", "verified_at": null, "failed_at": "2026-10-05T08:00:05+00:00", "expires_at": null, "failure_message": "Подписанный документ не совпадает с актуальными данными владельца." } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Дополнительные примеры из контракта ### cURL ```bash BASE_URL='https://api.b.websoft.kz/api/reseller/v1' # BASE_URL включает /api/reseller/v1. Подставьте реальные UUID своего окружения. # Для нового намеренного действия задайте новый IDEMPOTENCY_KEY; для повтора сохраните прежний. curl --request POST "$BASE_URL/domains/019a1234-1000-7000-8000-000000000001/registrant-verification" \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header "Accept: application/json" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header "Content-Type: application/json" \ --data-raw '{ "type": "eds", "payload_snapshot": { "domain-name": "example.kz", "domain-creation-time": "2026-10-01T08:00:00.000Z", "registrant-name": "Ivan Petrov", "registrant-org": null, "registrant-residencedetails-country": "KZ", "registrant-residencedetails-externalidtype": "IIN", "registrant-residencedetails-externalidvalue": "000000000000" }, "signable_payload": "[KK] ТІРКЕУШІНІҢ ТІРКЕУ ДЕРЕКТЕРІН РАСТАУ ТУРАЛЫ ӨТІНІШ\n\nОсы өтініш арқылы мен көрсетілген домендік атаудың тіркеу деректерінің өзектілігі мен дұрыстығын Қазақстан интернет\nсегментіндегі домендік атауларды тіркеу, пайдалану және бөлу қағидаларына сәйкес толық растаймын.\n\nОПЕРАЦИЯ ТУРАЛЫ ДЕРЕКТЕР:\n---------------------------------------------------------------------------\n* Домендік атау: example.kz\n* Құрылған күні: 2026-10-01T08:00:00.000Z\n* Тіркеуші (Т.А.Ә.): Ivan Petrov\n* Ұйым: \n* Ел: KZ\n* Құжат түрі: IIN\n* Құжат нөмірі: 000000000000\n\nБұл құжат Қазақстандық торап ақпарат орталығына (KazNIC) жіберуге арналған.\n\nKazNIC. Барлық құқықтар қорғалған.\n\n\n[RU] ЗАЯВЛЕНИЕ НА ПОДТВЕРЖДЕНИЕ РЕГИСТРАЦИОННЫХ ДАННЫХ\n\nНастоящим действием я в полной мере подтверждаю актуальность и достоверность регистрационных данных указанного\nдоменного имени в соответствии с Правилами регистрации, пользования и распределения доменных имен в пространстве\nказахстанского сегмента Интернета.\n\nДАННЫЕ ОПЕРАЦИИ:\n---------------------------------------------------------------------------\n* Доменное имя: example.kz\n* Дата создания: 2026-10-01T08:00:00.000Z\n* Регистрант (ФИО): Ivan Petrov\n* Организация: \n* Страна: KZ\n* Тип документа: IIN\n* Номер документа: 000000000000\n\nДанный документ предназначен для отправки в Казахстанский центр сетевой информации (KazNIC).\n\nKazNIC. Все права защищены.", "signature": "REPLACE_WITH_BASE64_DER_CADES_T_FROM_NCALAYER", "certificate": null, "eds_provider": "ncalayer", "signature_options": { "method": "kz.gov.pki.knca.basics.sign", "format": "cms", "decode": false, "encapsulate": false, "digested": false, "timestamp_applied": true, "cms_type": "CMS Detached", "cades_profile": "CAdES-T" } }' ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции ::: details Источники проверки контракта - `routes/api/reseller/v1.php` - `app/Application/ResellerApi/V1/Controller/DomainController.php` ::: --- SOURCE: /reference/listWebhooks.md # Список подписок `GET /webhooks` **operationId:** `listWebhooks` Порядок от новых к старым, cursor pagination. Только текущая среда. Scope: webhooks.read. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.read` ## Параметры ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" curl --request GET "$API_BASE_URL/webhooks?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks"; const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks" url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks" endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks"; URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Страница подписок, новые сначала. Схема: WebhookList | | `$.data` | array | Да | | Подписки текущего реселлера и среды; общие для его ключей с нужным scope. | | `$.data[]` | object | Да | required: ["uuid","environment","name","url","event_subscriptions","timeout_seconds","max_attempts","status","rotated_at","disabled_at","created_at"] | Настройки webhook; signing_secret здесь никогда не возвращается. Схема: Webhook | | `$.data[].uuid` | string | Да | format: "uuid" | UUID подписки. | | `$.data[].environment` | string | Да | enum: "test", "live" | Среда подписки; события другой среды не доставляются. | | `$.data[].name` | string | Да | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.data[].url` | string | Да | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.data[].event_subscriptions` | array | Да | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.data[].event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data[].timeout_seconds` | integer | Да | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.data[].max_attempts` | integer | Да | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | | `$.data[].status` | string | Да | enum: "active", "disabled" | Активность получателя. | | `$.data[].rotated_at` | string / null | Да | format: "date-time" | Последняя ротация секрета, null до первой ротации. | | `$.data[].disabled_at` | string / null | Да | format: "date-time" | Последнее отключение; null для активного. | | `$.data[].created_at` | string / null | Да | format: "date-time" | Создание подписки. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Схема: CursorMeta | | `$.meta.next_cursor` | string / null | Да | | Непрозрачный курсор следующей страницы; null означает конец. | | `$.meta.per_page` | integer | Да | minimum: 1; maximum: 100 | Фактический размер страницы; по умолчанию 25. | #### default (200) ```json { "data": [ { "uuid": "22222222-2222-4222-8222-222222222222", "environment": "test", "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8, "status": "active", "rotated_at": null, "disabled_at": null, "created_at": "2026-10-05T09:00:00+00:00" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### empty (200) ```json { "data": [], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/createWebhook.md # Создать подписку `POST /webhooks` **operationId:** `createWebhook` Подписка создаётся active. URL проверяется сейчас и перед каждой доставкой; создание не выполняет HTTP test. Секрет выдаётся в ответе, а не GET. Повтор того же HTTP-запроса в пределах TTL может вернуть сохранённый секрет. Scope: webhooks.manage. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.manage` ## Параметры ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["name","url","event_subscriptions"] | Создание активной подписки. environment берётся из Bearer, status передавать запрещено. Схема: WebhookCreate | | `$.name` | string | Да | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.url` | string | Да | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.event_subscriptions` | array | Да | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.timeout_seconds` | integer | Нет | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.max_attempts` | integer | Нет | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | #### default ```json { "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8 } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/webhooks" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8 }' ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks"; const url = baseUrl + path; const body = `{ "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8 }`; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks" url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"name\": \"Domain events\",\n \"url\": \"https://reseller.example/webhooks/billing\",\n \"event_subscriptions\": [\n \"domain.operation.updated\",\n \"domain.registration.updated\",\n \"domain.verification.updated\"\n ],\n \"timeout_seconds\": 10,\n \"max_attempts\": 8\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks" endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, strings.NewReader("{\n \"name\": \"Domain events\",\n \"url\": \"https://reseller.example/webhooks/billing\",\n \"event_subscriptions\": [\n \"domain.operation.updated\",\n \"domain.registration.updated\",\n \"domain.verification.updated\"\n ],\n \"timeout_seconds\": 10,\n \"max_attempts\": 8\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks"; URI uri = URI.create(baseUrl + path); String body = "{\n \"name\": \"Domain events\",\n \"url\": \"https://reseller.example/webhooks/billing\",\n \"event_subscriptions\": [\n \"domain.operation.updated\",\n \"domain.registration.updated\",\n \"domain.verification.updated\"\n ],\n \"timeout_seconds\": 10,\n \"max_attempts\": 8\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("POST", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks"; using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"name\": \"Domain events\",\n \"url\": \"https://reseller.example/webhooks/billing\",\n \"event_subscriptions\": [\n \"domain.operation.updated\",\n \"domain.registration.updated\",\n \"domain.verification.updated\"\n ],\n \"timeout_seconds\": 10,\n \"max_attempts\": 8\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | | 409 | `conflict` | Ключ уже обрабатывается или ранее использован с другим method/path/query/body. | Не меняйте ключ после неизвестного результата. Дождитесь исходной попытки; исправленный запрос оформляйте новым ключом только после сверки. | | 422 | `validation_failed` | Idempotency-Key отсутствует/пуст/длиннее 160 символов. | Передайте уникальный ключ на логическую операцию. | | 422 | `validation_failed` | Неверные name/url/events/timeout/max_attempts/status; URL не публичный HTTPS, DNS не разрешается или есть запрещённый адрес. | Исправьте поля из details, DNS и сертификат; запрос с исправленным телом отправляйте с новым ключом. | ### HTTP 201 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","signing_secret","secret_notice"] | Создание/ротация возвращает секрет. Последующие GET его не возвращают; HTTP replay с тем же ключом в пределах TTL может повторить этот ответ, включая секрет. Схема: WebhookSecretResponse | | `$.data` | object | Да | required: ["uuid","environment","name","url","event_subscriptions","timeout_seconds","max_attempts","status","rotated_at","disabled_at","created_at"] | Настройки webhook; signing_secret здесь никогда не возвращается. Схема: Webhook | | `$.data.uuid` | string | Да | format: "uuid" | UUID подписки. | | `$.data.environment` | string | Да | enum: "test", "live" | Среда подписки; события другой среды не доставляются. | | `$.data.name` | string | Да | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.data.url` | string | Да | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.data.event_subscriptions` | array | Да | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.data.event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data.timeout_seconds` | integer | Да | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.data.max_attempts` | integer | Да | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | | `$.data.status` | string | Да | enum: "active", "disabled" | Активность получателя. | | `$.data.rotated_at` | string / null | Да | format: "date-time" | Последняя ротация секрета, null до первой ротации. | | `$.data.disabled_at` | string / null | Да | format: "date-time" | Последнее отключение; null для активного. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создание подписки. | | `$.signing_secret` | string | Да | pattern: "^whsec_[A-Za-z0-9]{64}$" | Общий секрет HMAC, whsec_ плюс 64 символа. Храните зашифрованно; не публикуйте в браузере/логах/ИИ. | | `$.secret_notice` | string | Да | const: "This signing secret is shown once and cannot be recovered." | Текст предупреждения. Ограничение относится к GET; учтите HTTP replay. | #### default (201) ```json { "data": { "uuid": "22222222-2222-4222-8222-222222222222", "environment": "test", "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8, "status": "active", "rotated_at": null, "disabled_at": null, "created_at": "2026-10-05T09:00:00+00:00" }, "signing_secret": "whsec_EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0", "secret_notice": "This signing secret is shown once and cannot be recovered." } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/getWebhook.md # Получить подписку `GET /webhooks/{webhookUuid}` **operationId:** `getWebhook` Возвращает настройки, но не секрет. Scope: webhooks.read. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.read` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" curl --request GET "$API_BASE_URL/webhooks/${WEBHOOK_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Одна подписка. Схема: WebhookResponse | | `$.data` | object | Да | required: ["uuid","environment","name","url","event_subscriptions","timeout_seconds","max_attempts","status","rotated_at","disabled_at","created_at"] | Настройки webhook; signing_secret здесь никогда не возвращается. Схема: Webhook | | `$.data.uuid` | string | Да | format: "uuid" | UUID подписки. | | `$.data.environment` | string | Да | enum: "test", "live" | Среда подписки; события другой среды не доставляются. | | `$.data.name` | string | Да | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.data.url` | string | Да | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.data.event_subscriptions` | array | Да | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.data.event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data.timeout_seconds` | integer | Да | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.data.max_attempts` | integer | Да | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | | `$.data.status` | string | Да | enum: "active", "disabled" | Активность получателя. | | `$.data.rotated_at` | string / null | Да | format: "date-time" | Последняя ротация секрета, null до первой ротации. | | `$.data.disabled_at` | string / null | Да | format: "date-time" | Последнее отключение; null для активного. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создание подписки. | #### default (200) ```json { "data": { "uuid": "22222222-2222-4222-8222-222222222222", "environment": "test", "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8, "status": "active", "rotated_at": null, "disabled_at": null, "created_at": "2026-10-05T09:00:00+00:00" } } ``` #### disabled (200) ```json { "data": { "uuid": "22222222-2222-4222-8222-222222222222", "environment": "test", "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8, "status": "disabled", "rotated_at": null, "disabled_at": "2026-10-05T10:00:00+00:00", "created_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/updateWebhook.md # Изменить подписку `PATCH /webhooks/{webhookUuid}` **operationId:** `updateWebhook` Непереданные поля сохраняются. status=disabled при неизменном URL позволяет отключить получателя даже с недоступным DNS. Включение и остальные изменения перепроверяют URL. Очередные доставки используют актуальные URL/секрет/timeout/max_attempts; изменение подписок не отменяет уже созданные доставки. Scope: webhooks.manage. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.manage` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Обязательное. ### application/json | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: [] | Частичное изменение. Пропущенные поля сохраняются; null для перечисленных полей запрещён. Пустой объект допустим, но выполняется проверка URL. Для отключения достаточно status=disabled. Схема: WebhookUpdate | | `$.name` | string | Нет | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.url` | string | Нет | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.event_subscriptions` | array | Нет | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.timeout_seconds` | integer | Нет | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.max_attempts` | integer | Нет | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | | `$.status` | string | Нет | enum: "active", "disabled" | Выключение не удаляет настройки. Повторное включение не возвращает автоматически пропущенные события. | #### default ```json { "status": "disabled" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/webhooks/${WEBHOOK_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "status": "disabled" }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const body = `{ "status": "disabled" }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"status\": \"disabled\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"status\": \"disabled\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"status\": \"disabled\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"status\": \"disabled\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### enable ```json { "status": "active" } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/webhooks/${WEBHOOK_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "status": "active" }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const body = `{ "status": "active" }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"status\": \"active\"\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"status\": \"active\"\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"status\": \"active\"\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"status\": \"active\"\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: #### change ```json { "name": "Renewal monitor", "url": "https://reseller.example/webhooks/domains", "event_subscriptions": [ "domain.operation.updated" ], "timeout_seconds": 15, "max_attempts": 10 } ``` ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request PATCH "$API_BASE_URL/webhooks/${WEBHOOK_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Renewal monitor", "url": "https://reseller.example/webhooks/domains", "event_subscriptions": [ "domain.operation.updated" ], "timeout_seconds": 15, "max_attempts": 10 }' ``` ```php [PHP 8+] 'PATCH', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_POSTFIELDS => $body, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const body = `{ "name": "Renewal monitor", "url": "https://reseller.example/webhooks/domains", "event_subscriptions": [ "domain.operation.updated" ], "timeout_seconds": 15, "max_attempts": 10 }`; const response = await fetch(url, { method: "PATCH", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), "Content-Type": "application/json", }, body, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), "Content-Type": "application/json", } body = "{\n \"name\": \"Renewal monitor\",\n \"url\": \"https://reseller.example/webhooks/domains\",\n \"event_subscriptions\": [\n \"domain.operation.updated\"\n ],\n \"timeout_seconds\": 15,\n \"max_attempts\": 10\n}".encode("utf-8") request = Request(url, data=body, headers=headers, method="PATCH") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("PATCH", endpoint, strings.NewReader("{\n \"name\": \"Renewal monitor\",\n \"url\": \"https://reseller.example/webhooks/domains\",\n \"event_subscriptions\": [\n \"domain.operation.updated\"\n ],\n \"timeout_seconds\": 15,\n \"max_attempts\": 10\n}")) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) request.Header.Set("Content-Type", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); String body = "{\n \"name\": \"Renewal monitor\",\n \"url\": \"https://reseller.example/webhooks/domains\",\n \"event_subscriptions\": [\n \"domain.operation.updated\"\n ],\n \"timeout_seconds\": 15,\n \"max_attempts\": 10\n}"; HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .header("Content-Type", "application/json") .method("PATCH", HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8)) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("PATCH"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); request.Content = new StringContent("{\n \"name\": \"Renewal monitor\",\n \"url\": \"https://reseller.example/webhooks/domains\",\n \"event_subscriptions\": [\n \"domain.operation.updated\"\n ],\n \"timeout_seconds\": 15,\n \"max_attempts\": 10\n}", Encoding.UTF8, "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | | 409 | `conflict` | Ключ уже обрабатывается или ранее использован с другим method/path/query/body. | Не меняйте ключ после неизвестного результата. Дождитесь исходной попытки; исправленный запрос оформляйте новым ключом только после сверки. | | 422 | `validation_failed` | Idempotency-Key отсутствует/пуст/длиннее 160 символов. | Передайте уникальный ключ на логическую операцию. | | 422 | `validation_failed` | Неверные name/url/events/timeout/max_attempts/status; URL не публичный HTTPS, DNS не разрешается или есть запрещённый адрес. | Исправьте поля из details, DNS и сертификат; запрос с исправленным телом отправляйте с новым ключом. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Одна подписка. Схема: WebhookResponse | | `$.data` | object | Да | required: ["uuid","environment","name","url","event_subscriptions","timeout_seconds","max_attempts","status","rotated_at","disabled_at","created_at"] | Настройки webhook; signing_secret здесь никогда не возвращается. Схема: Webhook | | `$.data.uuid` | string | Да | format: "uuid" | UUID подписки. | | `$.data.environment` | string | Да | enum: "test", "live" | Среда подписки; события другой среды не доставляются. | | `$.data.name` | string | Да | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.data.url` | string | Да | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.data.event_subscriptions` | array | Да | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.data.event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data.timeout_seconds` | integer | Да | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.data.max_attempts` | integer | Да | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | | `$.data.status` | string | Да | enum: "active", "disabled" | Активность получателя. | | `$.data.rotated_at` | string / null | Да | format: "date-time" | Последняя ротация секрета, null до первой ротации. | | `$.data.disabled_at` | string / null | Да | format: "date-time" | Последнее отключение; null для активного. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создание подписки. | #### default (200) ```json { "data": { "uuid": "22222222-2222-4222-8222-222222222222", "environment": "test", "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8, "status": "disabled", "rotated_at": null, "disabled_at": "2026-10-05T10:00:00+00:00", "created_at": "2026-10-05T09:00:00+00:00" } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/deleteWebhook.md # Удалить подписку `DELETE /webhooks/{webhookUuid}` **operationId:** `deleteWebhook` Удаляет подписку и связанные записи доставок. Не удаляет домены и не отменяет доменные операции. Текущая сетевая попытка может уже выполняться. Повтор с тем же HTTP-ключом может вернуть сохранённый 204, с новым ключом после удаления ожидайте 404. Scope: webhooks.manage. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.manage` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request DELETE "$API_BASE_URL/webhooks/${WEBHOOK_UUID}" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" ``` ```php [PHP 8+] 'DELETE', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "DELETE", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), } body = None request = Request(url, data=body, headers=headers, method="DELETE") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("DELETE", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .method("DELETE", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("DELETE"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | | 409 | `conflict` | Ключ уже обрабатывается или ранее использован с другим method/path/query/body. | Не меняйте ключ после неизвестного результата. Дождитесь исходной попытки; исправленный запрос оформляйте новым ключом только после сверки. | | 422 | `validation_failed` | Idempotency-Key отсутствует/пуст/длиннее 160 символов. | Передайте уникальный ключ на логическую операцию. | ### HTTP 204 Удалено. Тело отсутствует; не пытайтесь декодировать JSON. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | Ответ без тела. ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/rotateWebhookSecret.md # Заменить секрет подписи `POST /webhooks/{webhookUuid}/rotate-secret` **operationId:** `rotateWebhookSecret` Новый секрет действует сразу, старый на стороне отправителя не сохраняется. Отложенные повторы подписываются новым; уже отправленный запрос может иметь старую подпись. GET секрет не выдаёт; HTTP replay может выдать сохранённый ответ. Ротация не включает disabled-подписку. Scope: webhooks.manage. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.manage` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/webhooks/${WEBHOOK_UUID}/rotate-secret" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}/rotate-secret"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}/rotate-secret" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), } body = None request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}/rotate-secret" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}/rotate-secret"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .method("POST", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}/rotate-secret"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | | 409 | `conflict` | Ключ уже обрабатывается или ранее использован с другим method/path/query/body. | Не меняйте ключ после неизвестного результата. Дождитесь исходной попытки; исправленный запрос оформляйте новым ключом только после сверки. | | 422 | `validation_failed` | Idempotency-Key отсутствует/пуст/длиннее 160 символов. | Передайте уникальный ключ на логическую операцию. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","signing_secret","secret_notice"] | Создание/ротация возвращает секрет. Последующие GET его не возвращают; HTTP replay с тем же ключом в пределах TTL может повторить этот ответ, включая секрет. Схема: WebhookSecretResponse | | `$.data` | object | Да | required: ["uuid","environment","name","url","event_subscriptions","timeout_seconds","max_attempts","status","rotated_at","disabled_at","created_at"] | Настройки webhook; signing_secret здесь никогда не возвращается. Схема: Webhook | | `$.data.uuid` | string | Да | format: "uuid" | UUID подписки. | | `$.data.environment` | string | Да | enum: "test", "live" | Среда подписки; события другой среды не доставляются. | | `$.data.name` | string | Да | minLength: 2; maxLength: 255 | Название для различения получателей. | | `$.data.url` | string | Да | format: "uri"; maxLength: 2048 | Публичный HTTPS URL. Все DNS A/AAAA должны быть глобальными адресами. Запрещены localhost, private/reserved/multicast/NAT64, userinfo, fragment. Redirect не выполняется. Сертификат и hostname проверяются. | | `$.data.event_subscriptions` | array | Да | minItems: 1; maxItems: 100 | Точные имена, не wildcard. Дубликаты удаляются при сохранении. Регистрация имени не гарантирует наличие отправителя. | | `$.data.event_subscriptions[]` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data.timeout_seconds` | integer | Да | minimum: 1; maximum: 30; default: 10 | Общий timeout одной попытки в секундах. Default 10, может изменяться провайдером. Connect timeout min(5,timeout_seconds). | | `$.data.max_attempts` | integer | Да | minimum: 1; maximum: 20; default: 8 | Максимум попыток, включая первую, по умолчанию 8 (настройка провайдера). После исчерпания статус failed, не бесконечные повторы. | | `$.data.status` | string | Да | enum: "active", "disabled" | Активность получателя. | | `$.data.rotated_at` | string / null | Да | format: "date-time" | Последняя ротация секрета, null до первой ротации. | | `$.data.disabled_at` | string / null | Да | format: "date-time" | Последнее отключение; null для активного. | | `$.data.created_at` | string / null | Да | format: "date-time" | Создание подписки. | | `$.signing_secret` | string | Да | pattern: "^whsec_[A-Za-z0-9]{64}$" | Общий секрет HMAC, whsec_ плюс 64 символа. Храните зашифрованно; не публикуйте в браузере/логах/ИИ. | | `$.secret_notice` | string | Да | const: "This signing secret is shown once and cannot be recovered." | Текст предупреждения. Ограничение относится к GET; учтите HTTP replay. | #### default (200) ```json { "data": { "uuid": "22222222-2222-4222-8222-222222222222", "environment": "test", "name": "Domain events", "url": "https://reseller.example/webhooks/billing", "event_subscriptions": [ "domain.operation.updated", "domain.registration.updated", "domain.verification.updated" ], "timeout_seconds": 10, "max_attempts": 8, "status": "active", "rotated_at": "2026-10-05T10:00:00+00:00", "disabled_at": null, "created_at": "2026-10-05T09:00:00+00:00" }, "signing_secret": "whsec_EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0EXAMPLE0", "secret_notice": "This signing secret is shown once and cannot be recovered." } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/testWebhook.md # Отправить тестовое событие `POST /webhooks/{webhookUuid}/test` **operationId:** `testWebhook` Создаёт событие webhook.test только для выбранной active-подписки, независимо от event_subscriptions. 202 означает очередь; прочитайте deliveries для подтверждения. Тело запроса не требуется. Scope: webhooks.manage. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.manage` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/webhooks/${WEBHOOK_UUID}/test" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}/test"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}/test" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), } body = None request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}/test" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}/test"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .method("POST", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}/test"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | | 409 | `conflict` | Ключ уже обрабатывается или ранее использован с другим method/path/query/body. | Не меняйте ключ после неизвестного результата. Дождитесь исходной попытки; исправленный запрос оформляйте новым ключом только после сверки. | | 422 | `validation_failed` | Idempotency-Key отсутствует/пуст/длиннее 160 символов. | Передайте уникальный ключ на логическую операцию. | | 409 | `conflict` | Подписка disabled. | Включите через PATCH, затем запросите test новым ключом. | ### HTTP 202 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Одна доставка, принятая в очередь; 202 не доказывает получение события. Схема: WebhookDeliveryResponse | | `$.data` | object | Да | required: ["uuid","event_uuid","event_type","status","attempt_number","http_status","error_code","next_attempt_at","delivered_at","failed_at"] | Текущее состояние доставки. Это не неизменяемый журнал каждой попытки; redeliver сбрасывает счётчик и поля результата той же доставки. Схема: WebhookDelivery | | `$.data.uuid` | string | Да | format: "uuid" | UUID доставки. Не входит в тело исходящего webhook. | | `$.data.event_uuid` | string / null | Да | format: "uuid" | ID события для дедупликации; null если связь недоступна. | | `$.data.event_type` | oneOf | Да | | Имя события. | | `$.data.event_type (oneOf 1)` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data.event_type (oneOf 2)` | null | Да | | | | `$.data.status` | string | Да | enum: "pending", "retry_scheduled", "delivered", "failed", "cancelled" | pending: ждёт; retry_scheduled: назначен повтор; delivered: 2xx; failed: исчерпаны попытки; cancelled: отключение/нарушение контекста. | | `$.data.attempt_number` | integer | Да | minimum: 0 | Завершённые попытки в текущем цикле, начинается с 0; redeliver сбрасывает. | | `$.data.http_status` | integer / null | Да | minimum: 100; maximum: 599 | Последний HTTP статус, null до ответа или при transport_error. | | `$.data.error_code` | string / null | Да | enum: null, "http_error", "transport_error", "webhook_disabled", "delivery_scope_mismatch" | Не HTTP error.code. http_error: не-2xx; transport_error: DNS/TLS/connect/timeout/URL; отмена: webhook_disabled/delivery_scope_mismatch. | | `$.data.next_attempt_at` | string / null | Да | format: "date-time" | Не раньше какого времени назначена попытка; не обещание точного времени. | | `$.data.delivered_at` | string / null | Да | format: "date-time" | Когда принят 2xx; иначе null. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Время failed/cancelled; иначе null. | #### default (202) ```json { "data": { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "pending", "attempt_number": 0, "http_status": null, "error_code": null, "next_attempt_at": "2026-10-05T09:01:00+00:00", "delivered_at": null, "failed_at": null } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/listWebhookDeliveries.md # Список доставок `GET /webhooks/{webhookUuid}/deliveries` **operationId:** `listWebhookDeliveries` Cursor pagination, новые сначала. Каждая строка отражает последнюю попытку, не историю всех HTTP-вызовов. response_excerpt, тело события, request_id и подпись здесь не раскрываются. Сохраняйте сырые входящие webhook безопасно у себя. Scope: webhooks.read. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.read` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### cursor Расположение: `query`. Необязательный. Непрозрачное значение meta.next_cursor. На первой странице не передаётся; не конструируйте вручную. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | | | ### per_page Расположение: `query`. Необязательный. Рекомендуется 1..100, по умолчанию 25. Реализация приводит к integer и ограничивает 1..100, не возвращает 422 за выход из диапазона. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1; maximum: 100; default: 25 | | **Пример** ```json 25 ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" curl --request GET "$API_BASE_URL/webhooks/${WEBHOOK_UUID}/deliveries?per_page=25" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' ``` ```php [PHP 8+] 'GET', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}/deliveries"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); const url = baseUrl + path + "?per_page=25"; const response = await fetch(url, { method: "GET", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}/deliveries" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) url = base_url + path + "?per_page=25" headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", } body = None request = Request(url, data=body, headers=headers, method="GET") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}/deliveries" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) endpoint := strings.TrimRight(baseURL, "/") + path + "?per_page=25" request, err := http.NewRequest("GET", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}/deliveries"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path + "?per_page=25"); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .method("GET", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}/deliveries"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("GET"), baseUrl + path + "?per_page=25"); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | ### HTTP 200 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data","meta"] | Страница доставок, новые сначала. Тела HTTP ответов и response_excerpt не раскрываются. Схема: WebhookDeliveryList | | `$.data` | array | Да | | | | `$.data[]` | object | Да | required: ["uuid","event_uuid","event_type","status","attempt_number","http_status","error_code","next_attempt_at","delivered_at","failed_at"] | Текущее состояние доставки. Это не неизменяемый журнал каждой попытки; redeliver сбрасывает счётчик и поля результата той же доставки. Схема: WebhookDelivery | | `$.data[].uuid` | string | Да | format: "uuid" | UUID доставки. Не входит в тело исходящего webhook. | | `$.data[].event_uuid` | string / null | Да | format: "uuid" | ID события для дедупликации; null если связь недоступна. | | `$.data[].event_type` | oneOf | Да | | Имя события. | | `$.data[].event_type (oneOf 1)` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data[].event_type (oneOf 2)` | null | Да | | | | `$.data[].status` | string | Да | enum: "pending", "retry_scheduled", "delivered", "failed", "cancelled" | pending: ждёт; retry_scheduled: назначен повтор; delivered: 2xx; failed: исчерпаны попытки; cancelled: отключение/нарушение контекста. | | `$.data[].attempt_number` | integer | Да | minimum: 0 | Завершённые попытки в текущем цикле, начинается с 0; redeliver сбрасывает. | | `$.data[].http_status` | integer / null | Да | minimum: 100; maximum: 599 | Последний HTTP статус, null до ответа или при transport_error. | | `$.data[].error_code` | string / null | Да | enum: null, "http_error", "transport_error", "webhook_disabled", "delivery_scope_mismatch" | Не HTTP error.code. http_error: не-2xx; transport_error: DNS/TLS/connect/timeout/URL; отмена: webhook_disabled/delivery_scope_mismatch. | | `$.data[].next_attempt_at` | string / null | Да | format: "date-time" | Не раньше какого времени назначена попытка; не обещание точного времени. | | `$.data[].delivered_at` | string / null | Да | format: "date-time" | Когда принят 2xx; иначе null. | | `$.data[].failed_at` | string / null | Да | format: "date-time" | Время failed/cancelled; иначе null. | | `$.meta` | object | Да | required: ["next_cursor","per_page"] | Схема: CursorMeta | | `$.meta.next_cursor` | string / null | Да | | Непрозрачный курсор следующей страницы; null означает конец. | | `$.meta.per_page` | integer | Да | minimum: 1; maximum: 100 | Фактический размер страницы; по умолчанию 25. | #### default (200) ```json { "data": [ { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "pending", "attempt_number": 0, "http_status": null, "error_code": null, "next_attempt_at": "2026-10-05T09:01:00+00:00", "delivered_at": null, "failed_at": null } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### httpFailure (200) ```json { "data": [ { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "retry_scheduled", "attempt_number": 1, "http_status": 503, "error_code": "http_error", "next_attempt_at": "2026-10-05T09:02:00+00:00", "delivered_at": null, "failed_at": null } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### transportFailure (200) ```json { "data": [ { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "failed", "attempt_number": 8, "http_status": null, "error_code": "transport_error", "next_attempt_at": null, "delivered_at": null, "failed_at": "2026-10-05T11:08:00+00:00" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### cancelled (200) ```json { "data": [ { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "cancelled", "attempt_number": 0, "http_status": null, "error_code": "webhook_disabled", "next_attempt_at": null, "delivered_at": null, "failed_at": "2026-10-05T09:02:00+00:00" } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` #### delivered (200) ```json { "data": [ { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "delivered", "attempt_number": 1, "http_status": 204, "error_code": null, "next_attempt_at": null, "delivered_at": "2026-10-05T09:01:01+00:00", "failed_at": null } ], "meta": { "next_cursor": null, "per_page": 25 } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/redeliverWebhook.md # Повторить завершённую доставку `POST /webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver` **operationId:** `redeliverWebhook` Допускаются delivered, failed и cancelled. Повтор использует ту же запись доставки и event id, сбрасывает attempt_number в 0 и ошибки/время завершения, отправляется на текущий URL с текущим секретом. Не создаёт новое бизнес-событие. Получатель должен дедуплицировать повтор. Scope: webhooks.manage. Bearer-ключ определяет реселлера и среду; подписки не принадлежат одному конкретному API-ключу. Общие правила ошибок и повторов см. руководство надёжности. **Scopes:** `webhooks.manage` ## Параметры ### webhookUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### deliveryUuid Расположение: `path`. Обязательный. UUID из ответа API. Передавайте корректную строку UUID. Ресурс другого реселлера или среды не раскрывается. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | ### Idempotency-Key Расположение: `header`. Обязательный. Обязателен для POST/PUT/PATCH/DELETE, включая check, sync, test, rotate-secret. После trim 1..160 символов. Один ключ на логическую операцию и API-ключ. Повторите те же method/path/query и байты JSON. TTL HTTP replay по умолчанию 24 часа от первого резервирования; не продлевается. Не переиспользуйте ключ для новой операции, доменная задача дополнительно сохраняет связь с ним. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | minLength: 1; maxLength: 160 | | **Пример** ```json "webhook-create-20261005-0001" ``` ## Тело запроса Тело запроса отсутствует. ## Пример запроса ::: code-group ```bash [cURL] API_BASE_URL='https://api.b.websoft.kz/api/reseller/v1' : "${RESELLER_API_TOKEN:?Set RESELLER_API_TOKEN in your environment}" : "${WEBHOOK_UUID:?Set WEBHOOK_UUID}" : "${DELIVERY_UUID:?Set DELIVERY_UUID}" : "${IDEMPOTENCY_KEY:?Set one persisted unique key per logical request}" curl --request POST "$API_BASE_URL/webhooks/${WEBHOOK_UUID}/deliveries/${DELIVERY_UUID}/redeliver" \ --connect-timeout 5 --max-time 20 --fail-with-body \ --header "Authorization: Bearer $RESELLER_API_TOKEN" \ --header 'Accept: application/json' \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" ``` ```php [PHP 8+] 'POST', CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_HEADERFUNCTION => static function (CurlHandle $curl, string $line) use (&$responseHeaders): int { if (str_starts_with($line, 'HTTP/')) { $responseHeaders = []; } elseif (str_contains($line, ':')) { [$name, $value] = explode(':', $line, 2); $responseHeaders[strtolower(trim($name))] = trim($value); } return strlen($line); }, ])) { throw new RuntimeException('Cannot configure cURL.'); } $raw = curl_exec($handle); if ($raw === false) { throw new RuntimeException('Transport failure; result may be unknown. cURL code: ' . curl_errno($handle)); } $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); } finally { unset($handle); } $requestId = $responseHeaders['x-request-id'] ?? null; $retryAfter = $responseHeaders['retry-after'] ?? null; $data = null; if ($raw !== '' && !in_array($status, [204, 205], true)) { try { $data = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); } catch (JsonException $error) { if ($status >= 200 && $status < 300) { throw new RuntimeException('Unexpected non-JSON success response.', 0, $error); } } } if ($status < 200 || $status >= 300) { // Inspect $data['error']; queue 429 using $retryAfter, never create a new write key blindly. throw new RuntimeException('HTTP ' . $status . '; request_id=' . ($requestId ?? '-') . '; retry_after=' . ($retryAfter ?? '-')); } // $data contains the decoded response; 202 means accepted, not completed. echo 'HTTP ' . $status . PHP_EOL; ``` ```js [JavaScript / Node.js] // Node.js 22+; save as request.mjs. No third-party dependencies. function requiredEnv(name) { const value = process.env[name]; if (!value || /[\r\n]/u.test(value)) throw new Error('Set a valid environment variable: ' + name); return value; } const baseUrl = (process.env.API_BASE_URL || "https://api.b.websoft.kz/api/reseller/v1").replace(/\/$/u, ''); let path = "/webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver"; path = path.replaceAll("{webhookUuid}", encodeURIComponent(requiredEnv("WEBHOOK_UUID"))); path = path.replaceAll("{deliveryUuid}", encodeURIComponent(requiredEnv("DELIVERY_UUID"))); const url = baseUrl + path; const response = await fetch(url, { method: "POST", redirect: 'manual', signal: AbortSignal.timeout(20_000), headers: { Authorization: 'Bearer ' + requiredEnv('RESELLER_API_TOKEN'), Accept: 'application/json', 'Idempotency-Key': requiredEnv('IDEMPOTENCY_KEY'), }, }); const requestId = response.headers.get('x-request-id'); const retryAfter = response.headers.get('retry-after'); const raw = await response.text(); let data = null; if (raw && ![204, 205].includes(response.status)) { try { data = JSON.parse(raw); } catch (cause) { if (response.ok) throw new Error('Unexpected non-JSON success response.', { cause }); } } if (!response.ok) { // Inspect data?.error; schedule 429 using retryAfter. Do not generate another write key. throw new Error('HTTP ' + response.status + '; request_id=' + requestId + '; retry_after=' + retryAfter); } // A timeout may have an unknown result. No automatic mutation retry in this example. // data contains the response; 202 means accepted, not completed. console.log('HTTP', response.status); ``` ```python [Python] # Python 3.10+; standard library only. import json import os from urllib.error import HTTPError from urllib.parse import quote from urllib.request import HTTPRedirectHandler, Request, build_opener def required_env(name): value = os.environ.get(name, "") if not value or "\r" in value or "\n" in value: raise RuntimeError("Set a valid environment variable: " + name) return value class NoRedirect(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): return None base_url = os.environ.get("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").rstrip("/") path = "/webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver" path = path.replace("{webhookUuid}", quote(required_env("WEBHOOK_UUID"), safe="")) path = path.replace("{deliveryUuid}", quote(required_env("DELIVERY_UUID"), safe="")) url = base_url + path headers = { "Authorization": "Bearer " + required_env("RESELLER_API_TOKEN"), "Accept": "application/json", "Idempotency-Key": required_env("IDEMPOTENCY_KEY"), } body = None request = Request(url, data=body, headers=headers, method="POST") opener = build_opener(NoRedirect()) try: response = opener.open(request, timeout=20) except HTTPError as error: response = error # HTTP failure still has a response body and headers. # Network/timeout exceptions propagate: a write may already have been accepted. with response: status = response.status request_id = response.headers.get("X-Request-ID") retry_after = response.headers.get("Retry-After") raw = response.read() data = None if raw and status not in (204, 205): try: data = json.loads(raw) except (ValueError, UnicodeDecodeError): if 200 <= status < 300: raise RuntimeError("Unexpected non-JSON success response.") if not 200 <= status < 300: # Inspect data['error']; schedule 429 with retry_after, keeping the same persisted key. raise RuntimeError(f"HTTP {status}; request_id={request_id}; retry_after={retry_after}") # data contains the response. Do not log secrets/PII; 202 is not completion. print("HTTP", status) ``` ```go [Go] // Go 1.22+; standard library only. Save as main.go and run: go run main.go package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "os" "strings" "time" ) func requiredEnv(name string) string { value := os.Getenv(name) if value == "" || strings.ContainsAny(value, "\r\n") { panic("Set a valid environment variable: " + name) } return value } func main() { baseURL := os.Getenv("API_BASE_URL") if baseURL == "" { baseURL = "https://api.b.websoft.kz/api/reseller/v1" } path := "/webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver" value0 := requiredEnv("WEBHOOK_UUID") path = strings.ReplaceAll(path, "{webhookUuid}", url.PathEscape(value0)) value1 := requiredEnv("DELIVERY_UUID") path = strings.ReplaceAll(path, "{deliveryUuid}", url.PathEscape(value1)) endpoint := strings.TrimRight(baseURL, "/") + path request, err := http.NewRequest("POST", endpoint, nil) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) request.Header.Set("Accept", "application/json") request.Header.Set("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } response, err := client.Do(request) if err != nil { panic("Transport failure; operation result may be unknown") } defer response.Body.Close() raw, err := io.ReadAll(response.Body) if err != nil { panic("Response read failed; operation result may be unknown") } requestID := response.Header.Get("X-Request-ID") retryAfter := response.Header.Get("Retry-After") var data any if len(raw) > 0 && response.StatusCode != 204 && response.StatusCode != 205 { decoder := json.NewDecoder(strings.NewReader(string(raw))) decoder.UseNumber() // Preserve integer money/identifiers without float rounding. if err := decoder.Decode(&data); err != nil && response.StatusCode >= 200 && response.StatusCode < 300 { panic("Unexpected non-JSON success response") } } if response.StatusCode < 200 || response.StatusCode >= 300 { // Inspect data; schedule 429 using retryAfter. Never create a new key blindly. panic(fmt.Sprintf("HTTP %d; request_id=%s; retry_after=%s", response.StatusCode, requestID, retryAfter)) } // data holds the JSON value; 202 means accepted, not completed. Do not log secrets. fmt.Println("HTTP", response.StatusCode) } ``` ```java [Java] // Java 17+; standard library only. Save as RequestExample.java. // Run: java RequestExample.java import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; public class RequestExample { private static String requiredEnv(String name) { String value = System.getenv(name); if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) { throw new IllegalStateException("Set a valid environment variable: " + name); } return value; } private static String envOr(String name, String fallback) { String value = System.getenv(name); return value == null || value.isEmpty() ? fallback : value; } public static void main(String[] args) throws Exception { String baseUrl = envOr("API_BASE_URL", "https://api.b.websoft.kz/api/reseller/v1").replaceAll("/+$", ""); String path = "/webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver"; path = path.replace("{webhookUuid}", URLEncoder.encode(requiredEnv("WEBHOOK_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); path = path.replace("{deliveryUuid}", URLEncoder.encode(requiredEnv("DELIVERY_UUID"), StandardCharsets.UTF_8).replace("+", "%20")); URI uri = URI.create(baseUrl + path); HttpRequest request = HttpRequest.newBuilder(uri) .timeout(Duration.ofSeconds(20)) .header("Authorization", "Bearer " + requiredEnv("RESELLER_API_TOKEN")) .header("Accept", "application/json") .header("Idempotency-Key", requiredEnv("IDEMPOTENCY_KEY")) .method("POST", HttpRequest.BodyPublishers.noBody()) .build(); HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .followRedirects(HttpClient.Redirect.NEVER) .build(); // Reuse HttpClient in production. A timeout may mean an unknown write result. HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); int status = response.statusCode(); String requestId = response.headers().firstValue("X-Request-ID").orElse(""); String retryAfter = response.headers().firstValue("Retry-After").orElse(""); String responseBody = (status == 204 || status == 205) ? "" : response.body(); if (status < 200 || status >= 300) { // Decode responseBody with your JSON library; schedule 429 using retryAfter. throw new IllegalStateException("HTTP " + status + "; request_id=" + requestId + "; retry_after=" + retryAfter); } // Java SE has no JSON object mapper: pass responseBody to your project's JSON parser. // Do not parse an empty 204 body; 202 means accepted, not completed. System.out.println("HTTP " + status); } } ``` ```csharp [C# / .NET] // C# / .NET 8+ console app; standard library only. Save as Program.cs. using System; using System.Net.Http; using System.Text; using System.Text.Json; static string RequiredEnv(string name) { string? value = Environment.GetEnvironmentVariable(name); if (string.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new InvalidOperationException("Set a valid environment variable: " + name); return value; } string baseUrl = (Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https://api.b.websoft.kz/api/reseller/v1").TrimEnd('/'); string path = "/webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver"; path = path.Replace("{webhookUuid}", Uri.EscapeDataString(RequiredEnv("WEBHOOK_UUID"))); path = path.Replace("{deliveryUuid}", Uri.EscapeDataString(RequiredEnv("DELIVERY_UUID"))); using var handler = new HttpClientHandler { AllowAutoRedirect = false }; using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(20) }; // Reuse HttpClient (or IHttpClientFactory) in production, not one client per request. using var request = new HttpRequestMessage(new HttpMethod("POST"), baseUrl + path); request.Headers.Add("Authorization", "Bearer " + RequiredEnv("RESELLER_API_TOKEN")); request.Headers.Add("Accept", "application/json"); request.Headers.Add("Idempotency-Key", RequiredEnv("IDEMPOTENCY_KEY")); // Transport exceptions may have an unknown result; do not issue a new mutation automatically. using var response = await client.SendAsync(request); int status = (int)response.StatusCode; string requestId = response.Headers.TryGetValues("X-Request-ID", out var ids) ? string.Join(",", ids) : ""; string retryAfter = response.Headers.TryGetValues("Retry-After", out var delays) ? string.Join(",", delays) : ""; string raw = await response.Content.ReadAsStringAsync(); JsonElement? data = null; if (raw.Length > 0 && status != 204 && status != 205) { try { using var document = JsonDocument.Parse(raw); data = document.RootElement.Clone(); } catch (JsonException) { if (response.IsSuccessStatusCode) throw; } } if (!response.IsSuccessStatusCode) { // Inspect data; schedule 429 using retryAfter and preserve the same key/body. throw new HttpRequestException($"HTTP {status}; request_id={requestId}; retry_after={retryAfter}"); } // data contains JSON, or null for 204. HTTP 202 is not completion. Console.WriteLine($"HTTP {status}"); ``` ::: ## Ответы ### Условия ошибок и восстановление | HTTP | Код | Когда возникает | Что делать | | --- | --- | --- | --- | | 401 | `unauthenticated` | Нет корректного активного Bearer-ключа либо он истёк/отозван. | Получите/исправьте ключ; не повторяйте без изменения причины. | | 403 | `forbidden` | Нет scope операции, IP запрещён, API не включён для реселлера или его статус/тариф запрещает запрос. | Сверьте GET /context и разрешения у провайдера. | | 404 | `resource_not_found` | API отключён; для UUID-маршрута также ресурс отсутствует, чужой или другой среды. | Проверьте UUID и среду ключа; не перебирайте чужие ресурсы. | | 429 | `rate_limit_exceeded` | Исчерпана общая квота этого API-ключа. | Ждите Retry-After секунд плюс jitter; запись повторяйте с тем же Idempotency-Key. | | 500 | `internal_error` | Внутренний сбой; некорректный UUID также может не пройти обработку как 422. | Сохраните X-Request-ID, передавайте валидный UUID. После записи сначала сверка состояния, не новая операция. | | 409 | `conflict` | Ключ уже обрабатывается или ранее использован с другим method/path/query/body. | Не меняйте ключ после неизвестного результата. Дождитесь исходной попытки; исправленный запрос оформляйте новым ключом только после сверки. | | 422 | `validation_failed` | Idempotency-Key отсутствует/пуст/длиннее 160 символов. | Передайте уникальный ключ на логическую операцию. | | 409 | `conflict` | Подписка disabled либо доставка pending/retry_scheduled (уже в очереди). | Для disabled включите подписку. Для ожидающей доставки дождитесь завершения; не запускайте параллельный redeliver. | ### HTTP 202 Успешный ответ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `X-RateLimit-Limit`**: Лимит API-ключа на 60 секунд. После прохождения ограничителя, включая replay. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 1 | | ```text X-RateLimit-Limit: 120 ``` **Заголовок `X-RateLimit-Remaining`**: Остаток после запроса. Снимок, не гарантия при конкуренции. Отсутствует в собственной ветке 429 и при раннем отказе. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | integer | Да | minimum: 0 | | ```text X-RateLimit-Remaining: 119 ``` **Заголовок `Idempotency-Replayed`**: Только при возврате сохранённого HTTP-ответа: true, не false. Отсутствие не доказывает, что доменная операция ранее не выполнялась. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | const: "true" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["data"] | Одна доставка, принятая в очередь; 202 не доказывает получение события. Схема: WebhookDeliveryResponse | | `$.data` | object | Да | required: ["uuid","event_uuid","event_type","status","attempt_number","http_status","error_code","next_attempt_at","delivered_at","failed_at"] | Текущее состояние доставки. Это не неизменяемый журнал каждой попытки; redeliver сбрасывает счётчик и поля результата той же доставки. Схема: WebhookDelivery | | `$.data.uuid` | string | Да | format: "uuid" | UUID доставки. Не входит в тело исходящего webhook. | | `$.data.event_uuid` | string / null | Да | format: "uuid" | ID события для дедупликации; null если связь недоступна. | | `$.data.event_type` | oneOf | Да | | Имя события. | | `$.data.event_type (oneOf 1)` | string | Да | enum: "customer.created", "customer.updated", "domain.created", "domain.registration.updated", "domain.operation.updated", "domain.updated", "domain.nameservers.updated", "domain.contacts.updated", "domain.verification.submitted", "domain.verification.updated", "domain.renewed", "domain.auto_renew.failed", "domain.transfer.requested", "domain.transfer.updated", "hosting.created", "hosting.updated", "order.created", "order.updated", "invoice.created", "invoice.updated", "payment.completed", "service.updated", "ticket.created", "ticket.updated", "webhook.test" | Допустимые подписки. Девять имён зарезервированы без действующих отправителей: domain.nameservers.updated, domain.contacts.updated, domain.renewed, domain.transfer.requested, hosting.created, hosting.updated, invoice.created, invoice.updated, payment.completed. Не ждите их для завершения операций. Схема: WebhookEventType | | `$.data.event_type (oneOf 2)` | null | Да | | | | `$.data.status` | string | Да | enum: "pending", "retry_scheduled", "delivered", "failed", "cancelled" | pending: ждёт; retry_scheduled: назначен повтор; delivered: 2xx; failed: исчерпаны попытки; cancelled: отключение/нарушение контекста. | | `$.data.attempt_number` | integer | Да | minimum: 0 | Завершённые попытки в текущем цикле, начинается с 0; redeliver сбрасывает. | | `$.data.http_status` | integer / null | Да | minimum: 100; maximum: 599 | Последний HTTP статус, null до ответа или при transport_error. | | `$.data.error_code` | string / null | Да | enum: null, "http_error", "transport_error", "webhook_disabled", "delivery_scope_mismatch" | Не HTTP error.code. http_error: не-2xx; transport_error: DNS/TLS/connect/timeout/URL; отмена: webhook_disabled/delivery_scope_mismatch. | | `$.data.next_attempt_at` | string / null | Да | format: "date-time" | Не раньше какого времени назначена попытка; не обещание точного времени. | | `$.data.delivered_at` | string / null | Да | format: "date-time" | Когда принят 2xx; иначе null. | | `$.data.failed_at` | string / null | Да | format: "date-time" | Время failed/cancelled; иначе null. | #### default (202) ```json { "data": { "uuid": "33333333-3333-4333-8333-333333333333", "event_uuid": "44444444-4444-4444-8444-444444444444", "event_type": "webhook.test", "status": "pending", "attempt_number": 0, "http_status": null, "error_code": null, "next_attempt_at": "2026-10-05T09:01:00+00:00", "delivered_at": null, "failed_at": null } } ``` ### HTTP 401 Отсутствует, неверен, отозван или просрочен Bearer-ключ. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (401) ```json { "error": { "code": "unauthenticated", "message": "API credential is missing or invalid.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 403 Нет scope, IP не разрешён, тариф/режим/статус реселлера запрещает действие либо test-ключ обращается к live-only ресурсу. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (403) ```json { "error": { "code": "forbidden", "message": "Source IP address is not allowed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### supportTest (403) ```json { "error": { "code": "forbidden", "message": "Support is available only to live credentials.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 404 Ресурс недоступен в контексте или весь reseller API отключён. Не отличайте чужое от отсутствующего. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (404) ```json { "error": { "code": "resource_not_found", "message": "Not Found", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 409 Конфликт ключа идемпотентности или состояния. Устраните причину. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (409) ```json { "error": { "code": "conflict", "message": "A request with this Idempotency-Key is already processing.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### changedRequest (409) ```json { "error": { "code": "conflict", "message": "Idempotency-Key was already used with a different request.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 422 Ошибка данных или обязательного заголовка. details может быть []. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (422) ```json { "error": { "code": "validation_failed", "message": "A valid Idempotency-Key header is required.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` #### fields (422) ```json { "error": { "code": "validation_failed", "message": "The name field is required.", "details": { "name": [ "The name field is required." ] }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 429 Квота API-ключа исчерпана. Ждите Retry-After. Собственный 429 не содержит X-RateLimit-Limit/Remaining/Reset. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Заголовок `Retry-After`**: На 429 API: целые секунды до сброса окна, не дата/Unix timestamp. При 0 добавьте jitter. Proxy может вернуть HTTP-date. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | pattern: "^[0-9]+$" | | ```text Retry-After: 42 ``` **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (429) ```json { "error": { "code": "rate_limit_exceeded", "message": "API rate limit exceeded.", "details": { "retry_after": 42 }, "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ### HTTP 500 Этот HTTP-ответ не определяет результат записи. Сначала сверка; повтор только с прежним ключом и точным телом. **Заголовок `X-Request-ID`**: UUID текущего запроса. Корректный входной X-Request-ID принимается в нижнем регистре, иначе создаётся новый. Не гарантирован для proxy/неизвестного маршрута. | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | string | Да | format: "uuid" | | **Content-Type:** `application/json` | Поле | Тип | Обязательно в родителе | Ограничения | Описание | | --- | --- | --- | --- | --- | | `$` | object | Да | required: ["error"] | Собственный JSON-конверт API, не RFC 9457. Структура и code не зависят от языка; message и details предназначены человеку. До выбора маршрута и на proxy этот конверт не гарантирован. Схема: ErrorEnvelope | | `$.error` | object | Да | required: ["code","message","details","request_id"] | | | `$.error.code` | string | Да | | Машинный код; не извлекайте причину из message. | | `$.error.message` | string | Да | | Человекочитаемое, частично локализуемое сообщение; не стабильный идентификатор ошибки. | | `$.error.details` | oneOf | Да | | Без подробностей возвращается [], не null и не {}. Валидация: {поле:[сообщения]}. Квота: {retry_after:секунды}. | | `$.error.details (oneOf 1)` | array | Да | maxItems: 0 | | | `$.error.details (oneOf 2)` | object | Да | | | | `$.error.details (oneOf 2).*` | array | Нет | | | | `$.error.details (oneOf 2).*[]` | string | Да | | | | `$.error.details (oneOf 3)` | object | Да | required: ["retry_after"]; Дополнительные поля запрещены | | | `$.error.details (oneOf 3).retry_after` | integer | Да | minimum: 0 | | | `$.error.request_id` | string / null | Да | format: "uuid" | UUID для диагностики или null без контекста. При HTTP replay тело может содержать исходный ID, заголовок X-Request-ID относится к текущей попытке. | #### default (500) ```json { "error": { "code": "internal_error", "message": "The request could not be completed.", "details": [], "request_id": "11111111-1111-4111-8111-111111111111" } } ``` ## Машиночитаемый контракт [OpenAPI JSON](/openapi.json) · [OpenAPI YAML](/reseller-api-openapi.yaml) · Markdown этой операции --- SOURCE: /reference/index.md # Справочник REST API Всего операций: **63**. | Метод | Путь | Описание | | --- | --- | --- | | `GET` | `/context` | [Контекст ключа и реселлера](./resellerContext) | | `GET` | `/catalog/products` | [Опубликованные продукты](./listProducts) | | `GET` | `/catalog/domain-zones` | [Опубликованные доменные зоны](./listDomainZones) | | `POST` | `/quotes` | [Рассчитать и зафиксировать котировку](./createQuote) | | `GET` | `/orders` | [Список заказов](./listOrders) | | `POST` | `/orders` | [Создать заказ по котировке](./createOrder) | | `POST` | `/hosting/orders` | [Создать заказ хостинга](./createHostingOrder) | | `GET` | `/orders/{orderUuid}` | [Получить заказ](./getOrder) | | `GET` | `/services` | [Список услуг](./listServices) | | `GET` | `/services/{serviceUuid}` | [Получить услугу](./getService) | | `GET` | `/customers` | [Список покупателей](./listCustomers) | | `POST` | `/customers` | [Создать покупателя](./createCustomer) | | `GET` | `/customers/{customerUuid}` | [Получить покупателя](./getCustomer) | | `PATCH` | `/customers/{customerUuid}` | [Обновить покупателя](./updateCustomer) | | `GET` | `/contacts` | [Контакты покупателя](./listContacts) | | `POST` | `/contacts` | [Создать контакт для домена](./createContact) | | `GET` | `/contacts/{contactUuid}` | [Получить контакт](./getContact) | | `PATCH` | `/contacts/{contactUuid}` | [Заменить данные контакта](./updateContact) | | `DELETE` | `/contacts/{contactUuid}` | [Удалить неиспользуемый контакт](./deleteContact) | | `GET` | `/balance` | [Закупочный баланс реселлера](./getBalance) | | `GET` | `/ledger` | [Проводки закупочного счёта](./listLedger) | | `GET` | `/tickets` | [Список обращений](./listTickets) | | `POST` | `/tickets` | [Создать обращение](./createTicket) | | `GET` | `/tickets/{ticketUuid}` | [Получить тикет и переписку](./getTicket) | | `POST` | `/tickets/{ticketUuid}/messages` | [Ответить и при необходимости закрыть тикет](./replyTicket) | | `GET` | `/domains` | [Список доменов](./listDomains) | | `POST` | `/domains` | [Создать заказ регистрации домена](./registerDomain) | | `GET` | `/domains/{domainUuid}` | [Получить домен](./getDomain) | | `DELETE` | `/domains/{domainUuid}` | [Удалить домен у регистратора](./deleteDomain) | | `POST` | `/domains/check` | [Проверить доступность имени](./checkDomain) | | `POST` | `/domains/{domainUuid}/renew` | [Продлить домен](./renewDomain) | | `POST` | `/domains/{domainUuid}/transfer` | [Запросить трансфер существующей локальной записи](./transferDomain) | | `POST` | `/domains/{domainUuid}/restore` | [Восстановить домен из периода восстановления](./restoreDomain) | | `PUT` | `/domains/{domainUuid}/nameservers` | [Заменить делегирование NS](./updateDomainNameservers) | | `PUT` | `/domains/{domainUuid}/contacts` | [Изменить контакты зарегистрированного домена](./updateDomainContacts) | | `PUT` | `/domains/{domainUuid}/statuses` | [Добавить или снять client-статусы](./updateDomainStatuses) | | `PUT` | `/domains/{domainUuid}/whois-privacy` | [Настроить скрытие контактных данных WHOIS](./setDomainWhoisPrivacy) | | `POST` | `/domains/{domainUuid}/sync` | [Обновить локальные сведения из реестра](./synchronizeDomain) | | `POST` | `/domains/{domainUuid}/auth-code` | [Обновить и получить код трансфера](./refreshDomainAuthCode) | | `POST` | `/domains/{domainUuid}/transfer/query` | [Запросить состояние текущего трансфера](./queryDomainTransfer) | | `POST` | `/domains/{domainUuid}/transfer/approve` | [Подтвердить текущий трансфер](./approveDomainTransfer) | | `POST` | `/domains/{domainUuid}/transfer/reject` | [Отклонить текущий трансфер](./rejectDomainTransfer) | | `POST` | `/domains/{domainUuid}/transfer/cancel` | [Отменить текущий запрос трансфера](./cancelDomainTransfer) | | `GET` | `/domains/{domainUuid}/hosts` | [Прочитать один subordinate host из реестра](./getDomainHost) | | `POST` | `/domains/{domainUuid}/hosts` | [Создать subordinate glue-host](./createDomainHost) | | `PUT` | `/domains/{domainUuid}/hosts` | [Изменить адреса glue-host](./updateDomainHost) | | `DELETE` | `/domains/{domainUuid}/hosts` | [Удалить subordinate glue-host](./deleteDomainHost) | | `POST` | `/domains/transfers` | [Перенести домен, которого ещё нет в биллинге](./transferDomainIn) | | `GET` | `/domains/{domainUuid}/capabilities` | [Проверить поддерживаемые операции](./getDomainCapabilities) | | `PUT` | `/domains/{domainUuid}/auto-renew` | [Включить или выключить автопродление](./setDomainAutoRenew) | | `GET` | `/operations/{operationUuid}` | [Прочитать состояние доменной операции](./getDomainOperation) | | `GET` | `/domains/{domainUuid}/operations` | [История доменных операций](./listDomainOperations) | | `GET` | `/domains/{domainUuid}/registrant-verification/payload` | [Получить точный документ для ЭЦП владельца](./getRegistrantVerificationPayload) | | `POST` | `/domains/{domainUuid}/registrant-verification` | [Отправить ЭЦП и создать попытку проверки владельца](./submitRegistrantVerification) | | `GET` | `/webhooks` | [Список подписок](./listWebhooks) | | `POST` | `/webhooks` | [Создать подписку](./createWebhook) | | `GET` | `/webhooks/{webhookUuid}` | [Получить подписку](./getWebhook) | | `PATCH` | `/webhooks/{webhookUuid}` | [Изменить подписку](./updateWebhook) | | `DELETE` | `/webhooks/{webhookUuid}` | [Удалить подписку](./deleteWebhook) | | `POST` | `/webhooks/{webhookUuid}/rotate-secret` | [Заменить секрет подписи](./rotateWebhookSecret) | | `POST` | `/webhooks/{webhookUuid}/test` | [Отправить тестовое событие](./testWebhook) | | `GET` | `/webhooks/{webhookUuid}/deliveries` | [Список доставок](./listWebhookDeliveries) | | `POST` | `/webhooks/{webhookUuid}/deliveries/{deliveryUuid}/redeliver` | [Повторить завершённую доставку](./redeliverWebhook) | --- SOURCE: /examples/register.mjs ```js import { mkdir, open, readFile, rename, unlink, writeFile } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { createClient, createIntent } from './transport.mjs'; // Sandbox-only teaching workflow. Durable production storage must be transactional. const [configFile, stateDirectory, confirmation] = process.argv.slice(2); if (!configFile || !stateDirectory || confirmation !== '--execute-test') { throw new Error('Usage: node register.mjs input.json /private/state/directory --execute-test'); } const config = JSON.parse(await readFile(configFile, 'utf8')); const baseUrl = process.env.RESELLER_API_BASE; const token = process.env.RESELLER_API_TOKEN; const replayWindowSeconds = Number(process.env.RESELLER_REPLAY_WINDOW_SECONDS ?? 0); if (!Number.isSafeInteger(replayWindowSeconds) || replayWindowSeconds < 0) { throw new Error('RESELLER_REPLAY_WINDOW_SECONDS must be a non-negative integer confirmed against the server idempotency TTL.'); } if (!token?.startsWith('rsl_test_')) throw new Error('This example refuses live credentials.'); if (!config.zone_uuid || !config.domain_name || !config.domain_external_id || !config.order_external_id) throw new Error('Missing registration input.'); const client = createClient({ baseUrl, token }); const { body: context } = await client.request(createIntent('GET', '/context')); if (context.data.credential.environment !== 'test') throw new Error('Server did not confirm test environment.'); if (!['managed', 'aggregate'].includes(context.data.provider.customer_mode)) throw new Error('Unsupported customer mode.'); const directory = resolve(stateDirectory); await mkdir(directory, { recursive: true, mode: 0o700 }); const lockPath = join(directory, 'run.lock'); const lock = await open(lockPath, 'wx', 0o600); const statePath = join(directory, 'state.json'); try { let state; try { state = JSON.parse(await readFile(statePath, 'utf8')); } catch (error) { if (error.code !== 'ENOENT') throw error; state = { baseUrl, credentialUuid: context.data.credential.uuid, config, steps: {} }; } if (JSON.stringify(state.config) !== JSON.stringify(config) || state.baseUrl !== baseUrl || state.credentialUuid !== context.data.credential.uuid) throw new Error('State belongs to another workflow/credential. Do not overwrite it.'); const save = async () => { const temporary = `${statePath}.tmp`; await writeFile(temporary, JSON.stringify(state, null, 2), { mode: 0o600 }); await rename(temporary, statePath); }; const step = async (name, path, body) => { const resumed = Boolean(state.steps[name]); if (!state.steps[name]) { state.steps[name] = { createdAt: new Date().toISOString(), intent: createIntent('POST', path, body) }; await save(); } const stored = state.steps[name]; if (!stored.response) { const ageMs = Date.now() - Date.parse(stored.createdAt); if (resumed && (!Number.isFinite(ageMs) || ageMs < 0 || replayWindowSeconds === 0 || ageMs >= replayWindowSeconds * 1000)) { throw new Error('Unconfirmed intent cannot be replayed without a valid operator-confirmed replay window. Reconcile UUID/external_id first.'); } stored.response = await client.request(stored.intent); await save(); } return stored.response.body.data; }; const managed = context.data.provider.customer_mode === 'managed'; let customerUuid = managed ? config.customer_uuid : undefined; if (managed && !customerUuid) { if (!config.customer) throw new Error('Managed mode requires customer or customer_uuid.'); customerUuid = (await step('customer', '/customers', config.customer)).uuid; } const customer = managed ? { customer_uuid: customerUuid } : {}; let contactUuid = config.contact_uuid; if (!contactUuid) { if (!config.contact) throw new Error('Provide a valid sandbox contact or contact_uuid.'); contactUuid = (await step('contact', '/contacts', { ...config.contact, customer_uuid: customerUuid })).uuid; } const quote = await step('quote', '/quotes', { ...customer, items: [{ resource_type: 'domain_zone', resource_uuid: config.zone_uuid, operation: 'register', period_unit: 'year', period_count: config.years ?? 1, quantity: 1, currency: config.currency ?? 'KZT', payload: { domain_name: config.domain_name, external_id: config.domain_external_id, contact_uuids: { owner: contactUuid, admin: contactUuid, tech: contactUuid, billing: contactUuid }, nameservers: config.nameservers } }], }); if (!Number.isSafeInteger(config.max_total_minor) || !Number.isSafeInteger(quote.total_minor) || quote.total_minor > config.max_total_minor || quote.currency !== (config.currency ?? 'KZT')) { throw new Error('Quote exceeds the configured amount/currency, or amount is not an integer. Review it without issuing an order.'); } const order = await step('order', '/domains', { ...customer, quote_uuid: quote.uuid, external_id: config.order_external_id }); const current = await client.request(createIntent('GET', `/orders/${encodeURIComponent(order.uuid)}`)); const domains = await client.request(createIntent('GET', `/domains?external_id=${encodeURIComponent(config.domain_external_id)}`)); console.log(JSON.stringify({ order_uuid: order.uuid, status: current.body.data.status, domains: domains.body.data.map(domain => ({ uuid: domain.uuid, registration_status: domain.registration_status })) }, null, 2)); console.log('Order accepted, not necessarily registered. Resume GET reconciliation; do not create another intent.'); } finally { await lock.close(); await unlink(lockPath); } ``` --- SOURCE: /examples/register.test.mjs ```js import test from 'node:test'; import assert from 'node:assert/strict'; import { createServer } from 'node:http'; import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawn } from 'node:child_process'; const uuid = '11111111-1111-4111-8111-111111111111'; const config = { zone_uuid: uuid, domain_name: 'example.test', domain_external_id: 'domain-test', order_external_id: 'order-test', currency: 'KZT', max_total_minor: 10000, customer: { external_id: 'customer-test' }, contact_uuid: uuid, nameservers: [{ hostname: 'ns1.example.net' }, { hostname: 'ns2.example.net' }] }; async function scenario(t, settings = {}) { const directory = await mkdtemp(join(tmpdir(), 'reseller-docs-example-')); const input = join(directory, 'input.json'); await writeFile(input, JSON.stringify(config)); const requests = []; let credentialUuid = uuid; const server = createServer(async (request, response) => { const chunks = []; for await (const chunk of request) chunks.push(chunk); const rawBody = Buffer.concat(chunks).toString(); requests.push({ method: request.method, url: request.url, rawBody, key: request.headers['idempotency-key'] }); const path = request.url.split('?')[0]; let data; if (path.endsWith('/context')) data = { provider: { customer_mode: settings.mode ?? 'managed' }, credential: { uuid: credentialUuid, environment: 'test' } }; else if (path.endsWith('/quotes')) data = { uuid, currency: 'KZT', total_minor: settings.price ?? 5000 }; else if (path.endsWith('/domains') && request.method === 'GET') data = [{ uuid, registration_status: 'pending' }]; else data = { uuid, status: 'processing' }; response.writeHead(request.method === 'POST' ? 201 : 200, { 'Content-Type': 'application/json' }); response.end(JSON.stringify({ data })); }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); t.after(async () => { await new Promise(resolve => server.close(resolve)); await rm(directory, { recursive: true, force: true }); }); const stateDirectory = join(directory, 'state'); const run = (extraEnv = {}) => new Promise((resolve, reject) => { const child = spawn(process.execPath, [fileURLToPath(new URL('./register.mjs', import.meta.url)), input, stateDirectory, '--execute-test'], { env: { ...process.env, RESELLER_API_BASE: `http://127.0.0.1:${server.address().port}/api/reseller/v1`, RESELLER_API_TOKEN: 'rsl_test_example.not-a-secret', ...extraEnv }, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.on('data', chunk => { stdout += chunk; }); child.stderr.on('data', chunk => { stderr += chunk; }); child.on('error', reject); child.on('close', code => resolve({ code, stdout, stderr })); }); return { run, requests, stateDirectory, changeCredential: () => { credentialUuid = '22222222-2222-4222-8222-222222222222'; } }; } test('registration restart reads saved results without a second write', async t => { const { run, requests, stateDirectory } = await scenario(t); const first = await run(); assert.equal(first.code, 0, first.stderr); assert.equal(requests.filter(request => request.method === 'POST').length, 3); const second = await run(); assert.equal(second.code, 0, second.stderr); assert.equal(requests.filter(request => request.method === 'POST').length, 3); const state = JSON.parse(await readFile(join(stateDirectory, 'state.json'), 'utf8')); assert.equal(Object.keys(state.steps).length, 3); assert.ok(!JSON.stringify(state).includes('not-a-secret')); for (const request of requests.filter(request => request.method === 'POST')) assert.ok(request.key); }); test('aggregate registration omits customer creation and customer_uuid', async t => { const { run, requests } = await scenario(t, { mode: 'aggregate' }); const result = await run(); assert.equal(result.code, 0, result.stderr); assert.equal(requests.filter(request => request.url.endsWith('/customers')).length, 0); for (const request of requests.filter(request => request.method === 'POST')) { assert.equal(Object.hasOwn(JSON.parse(request.rawBody), 'customer_uuid'), false); } }); test('quote above agreed amount prevents order submission', async t => { const { run, requests } = await scenario(t, { price: 15000 }); const result = await run(); assert.notEqual(result.code, 0); assert.match(result.stderr, /Quote exceeds/); assert.equal(requests.filter(request => request.method === 'POST' && request.url.endsWith('/domains')).length, 0); }); test('example refuses live credential before any network request', async t => { const { run, requests } = await scenario(t); const result = await run({ RESELLER_API_TOKEN: 'rsl_live_example.not-a-secret' }); assert.notEqual(result.code, 0); assert.equal(requests.length, 0); }); test('state cannot be replayed under another credential', async t => { const { run, requests, changeCredential } = await scenario(t); assert.equal((await run()).code, 0); const count = requests.filter(request => request.method === 'POST').length; changeCredential(); const result = await run(); assert.notEqual(result.code, 0); assert.match(result.stderr, /another workflow\/credential/); assert.equal(requests.filter(request => request.method === 'POST').length, count); }); test('unconfirmed resume requires an operator-confirmed replay window', async t => { const { run, requests, stateDirectory } = await scenario(t); assert.equal((await run()).code, 0); const path = join(stateDirectory, 'state.json'); const state = JSON.parse(await readFile(path, 'utf8')); delete state.steps.order.response; await writeFile(path, JSON.stringify(state)); const count = requests.filter(request => request.method === 'POST').length; const result = await run({ RESELLER_REPLAY_WINDOW_SECONDS: '' }); assert.notEqual(result.code, 0); assert.match(result.stderr, /operator-confirmed replay window/); assert.equal(requests.filter(request => request.method === 'POST').length, count); }); test('authorized recent resume uses the original key and bytes', async t => { const { run, requests, stateDirectory } = await scenario(t); assert.equal((await run()).code, 0); const path = join(stateDirectory, 'state.json'); const state = JSON.parse(await readFile(path, 'utf8')); const intent = state.steps.order.intent; delete state.steps.order.response; await writeFile(path, JSON.stringify(state)); const result = await run({ RESELLER_REPLAY_WINDOW_SECONDS: '60' }); assert.equal(result.code, 0, result.stderr); const orders = requests.filter(request => request.method === 'POST' && request.url.endsWith('/domains')); assert.equal(orders.length, 2); assert.equal(orders[1].key, intent.key); assert.equal(orders[1].rawBody, intent.rawBody); }); test('expired replay window cannot issue a second mutation', async t => { const { run, requests, stateDirectory } = await scenario(t); assert.equal((await run()).code, 0); const path = join(stateDirectory, 'state.json'); const state = JSON.parse(await readFile(path, 'utf8')); delete state.steps.order.response; state.steps.order.createdAt = new Date(Date.now() - 120000).toISOString(); await writeFile(path, JSON.stringify(state)); const count = requests.filter(request => request.method === 'POST').length; const result = await run({ RESELLER_REPLAY_WINDOW_SECONDS: '60' }); assert.notEqual(result.code, 0); assert.equal(requests.filter(request => request.method === 'POST').length, count); }); ``` --- SOURCE: /examples/registration-input.json ```text { "zone_uuid": "22222222-2222-4222-8222-222222222222", "domain_name": "REPLACE_WITH_ASSIGNED_SANDBOX_DOMAIN", "domain_external_id": "domain-1001", "order_external_id": "order-1001", "currency": "KZT", "years": 1, "max_total_minor": 1000000, "customer": { "external_id": "customer-1001", "type": "individual", "display_name": "Ivan Petrov", "email": "owner@example.net", "first_name": "Ivan", "last_name": "Petrov", "preferred_currency": "KZT", "locale": "ru", "timezone": "Asia/Almaty" }, "contact_uuid": "33333333-3333-4333-8333-333333333333", "nameservers": [{ "hostname": "ns1.example.net" }, { "hostname": "ns2.example.net" }] } ``` --- SOURCE: /examples/transport.mjs ```js import { randomUUID } from 'node:crypto'; import { setTimeout as sleep } from 'node:timers/promises'; export class ApiError extends Error { constructor(status, body, requestId, retryAfter) { super(`Reseller API HTTP ${status}: ${body?.error?.code ?? 'unexpected_response'}`); this.status = status; this.body = body; this.requestId = requestId; this.retryAfter = retryAfter; } } export function createIntent(method, path, body, key = randomUUID()) { return { method: method.toUpperCase(), path, key, rawBody: body === undefined ? undefined : JSON.stringify(body) }; } export function retryDelay(value, now = Date.now()) { if (!value) return null; if (/^\d+$/.test(value)) return Number(value) * 1000; const date = Date.parse(value); return Number.isFinite(date) ? Math.max(0, date - now) : null; } export function createClient({ baseUrl, token, fetchImpl = fetch, wait = sleep, attempts = 3, timeoutMs = 20000 }) { const base = new URL(baseUrl); const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(base.hostname); if ((base.protocol !== 'https:' && !(base.protocol === 'http:' && loopback)) || base.username || base.password || base.search || base.hash) { throw new Error('Use a trusted HTTPS API base URL (HTTP only for loopback).'); } if (!token || !Number.isInteger(attempts) || attempts < 1 || attempts > 5) throw new Error('Invalid client configuration.'); async function request(intent) { if (!intent.path.startsWith('/') || intent.path.startsWith('//') || /[\\#\r\n]/.test(intent.path)) throw new Error('Invalid relative API path.'); const url = new URL(`${base.href.replace(/\/$/, '')}${intent.path}`); if (url.origin !== base.origin || !url.pathname.startsWith(`${base.pathname.replace(/\/$/, '')}/`)) throw new Error('Path escapes API base.'); const safe = ['GET', 'HEAD', 'OPTIONS'].includes(intent.method); if (!safe && (!intent.key || intent.key.length > 160)) throw new Error('Persist a valid idempotency key before sending.'); let failure; for (let attempt = 0; attempt < attempts; attempt++) { let delay = null; try { const response = await fetchImpl(url, { method: intent.method, redirect: 'error', signal: AbortSignal.timeout(timeoutMs), headers: { Authorization: `Bearer ${token}`, Accept: 'application/json', ...(!safe ? { 'Idempotency-Key': intent.key } : {}), ...(intent.rawBody !== undefined ? { 'Content-Type': 'application/json' } : {}), }, body: intent.rawBody, }); const raw = await response.text(); let body; try { body = JSON.parse(raw); } catch { body = null; } const requestId = response.headers.get('x-request-id'); if (response.ok) { if ([204, 205].includes(response.status) || intent.method === 'HEAD') { return { status: response.status, body: null, requestId, replayed: response.headers.get('idempotency-replayed') === 'true' }; } if (body === null) throw new ApiError(response.status, null, requestId, null); return { status: response.status, body, requestId, replayed: response.headers.get('idempotency-replayed') === 'true' }; } delay = retryDelay(response.headers.get('retry-after')); failure = new ApiError(response.status, body, requestId, delay); if (response.status !== 429 && response.status < 500) throw failure; } catch (error) { if (error instanceof ApiError) throw error; // A timeout is ambiguous: retain the same persisted intent and bytes. failure = error; } if (attempt + 1 < attempts) { const backoff = Math.min(30000, 1000 * 2 ** attempt) + Math.floor(Math.random() * 500); // Long server pauses are returned to the durable scheduler, never shortened. if (delay !== null && delay > 60000) throw failure; await wait(Math.max(backoff, delay ?? 0)); } } throw failure; } async function operation(uuid, { polls = 10, intervalMs = 5000 } = {}) { if (!/^[0-9a-f-]{36}$/i.test(uuid)) throw new Error('Invalid operation UUID.'); for (let poll = 0; poll < polls; poll++) { const { body } = await request(createIntent('GET', `/operations/${uuid}`)); const current = body.data; if (!['pending', 'processing'].includes(current?.status)) return current; if (poll + 1 < polls) await wait(intervalMs + Math.floor(Math.random() * 500)); } throw new Error('Polling budget exhausted. Persist the UUID and schedule a later GET; do not repeat the mutation.'); } return { request, operation }; } ``` --- SOURCE: /examples/transport.test.mjs ```js import test from 'node:test'; import assert from 'node:assert/strict'; import { createClient, createIntent, ApiError, retryDelay } from './transport.mjs'; const base = { baseUrl: 'https://api.example.net/api/reseller/v1', token: 'rsl_test_example.not-a-secret', wait: async () => {} }; const json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), { status, headers }); test('network retry preserves bytes, key, method and refuses redirects', async () => { const calls = []; const client = createClient({ ...base, fetchImpl: async (url, options) => { calls.push({ url: url.href, ...options }); if (calls.length === 1) throw new TypeError('network interrupted'); return json({ data: { uuid: 'accepted' } }, 202); } }); const intent = createIntent('POST', '/domains', { quote_uuid: 'example' }, 'durable-key'); const result = await client.request(intent); assert.equal(result.status, 202); assert.equal(calls.length, 2); for (const call of calls) { assert.equal(call.body, intent.rawBody); assert.equal(call.headers['Idempotency-Key'], 'durable-key'); assert.equal(call.redirect, 'error'); } }); test('429 respects Retry-After and reuses the intent', async () => { const delays = []; let calls = 0; const client = createClient({ ...base, wait: async delay => delays.push(delay), fetchImpl: async () => { if (++calls === 1) return json({ error: { code: 'rate_limit_exceeded' } }, 429, { 'Retry-After': '7' }); return json({ data: [] }); } }); await client.request(createIntent('GET', '/domains')); assert.equal(calls, 2); assert.ok(delays[0] >= 7000); }); test('long Retry-After returns to scheduler without shortening it', async () => { let calls = 0; const client = createClient({ ...base, fetchImpl: async () => { calls++; return json({}, 429, { 'Retry-After': '3600' }); } }); await assert.rejects(client.request(createIntent('GET', '/domains')), error => error instanceof ApiError && error.retryAfter === 3600000); assert.equal(calls, 1); }); test('conflict and validation errors are not blindly retried', async () => { for (const status of [401, 403, 404, 409, 422]) { let calls = 0; const client = createClient({ ...base, fetchImpl: async () => { calls++; return json({ error: { code: 'conflict' } }, status); } }); await assert.rejects(client.request(createIntent('POST', '/domains', {})), error => error.status === status); assert.equal(calls, 1); } }); test('non-JSON 502 has bounded retries and retains diagnostic request ID', async () => { let calls = 0; const client = createClient({ ...base, fetchImpl: async () => { calls++; return new Response('Bad gateway', { status: 502, headers: { 'X-Request-ID': 'proxy-1' } }); } }); await assert.rejects(client.request(createIntent('POST', '/domains', {})), error => error.status === 502 && error.body === null && error.requestId === 'proxy-1'); assert.equal(calls, 3); }); test('mutation needs a persisted key and escaped paths never receive token', async () => { const client = createClient({ ...base, fetchImpl: async () => { assert.fail('must not send'); } }); await assert.rejects(client.request({ method: 'POST', path: '/domains', rawBody: '{}' })); for (const path of ['//attacker.example', '/../other', '/%2e%2e/other', '/domains#fragment', '/\\attacker.example']) { await assert.rejects(client.request(createIntent('GET', path))); } assert.throws(() => createClient({ ...base, baseUrl: 'http://external.example/api/reseller/v1' })); }); test('replay metadata does not turn accepted operation into completion', async () => { const client = createClient({ ...base, fetchImpl: async () => json({ data: { status: 'pending' } }, 202, { 'Idempotency-Replayed': 'true' }) }); const result = await client.request(createIntent('POST', '/domains/example/renew', {})); assert.equal(result.replayed, true); assert.equal(result.body.data.status, 'pending'); }); test('uncertain operation immediately returns for manual/scheduled reconciliation', async () => { let calls = 0; const client = createClient({ ...base, fetchImpl: async () => { calls++; return json({ data: { status: 'uncertain' } }); } }); const result = await client.operation('66666666-6666-4666-8666-666666666666'); assert.equal(result.status, 'uncertain'); assert.equal(calls, 1); }); test('pending polling is bounded and only uses GET', async () => { let calls = 0; const client = createClient({ ...base, fetchImpl: async (_, options) => { calls++; assert.equal(options.method, 'GET'); return json({ data: { status: 'pending' } }); } }); await assert.rejects(client.operation('66666666-6666-4666-8666-666666666666', { polls: 2 }), /Polling budget exhausted/); assert.equal(calls, 2); }); test('Retry-After parser supports seconds, HTTP-date and invalid values', () => { assert.equal(retryDelay('3'), 3000); assert.equal(retryDelay('Thu, 01 Jan 1970 00:00:05 GMT', 1000), 4000); assert.equal(retryDelay('nonsense'), null); }); test('204 webhook deletion succeeds without JSON decoding', async () => { const client = createClient({ ...base, fetchImpl: async () => new Response(null, { status: 204 }) }); const response = await client.request(createIntent('DELETE', '/webhooks/33333333-3333-4333-8333-333333333333', { reason: 'Retire receiver' })); assert.equal(response.status, 204); assert.equal(response.body, null); }); ```