
# Adal Outbound

Adal Outbound — отдельный контур для исходящей HTTP-доставки. Ваше приложение передаёт Adal адрес получателя, метод, заголовки и тело запроса, а Adal принимает сообщение в очередь, выполняет доставку, повторяет неудачные попытки и сохраняет их результаты.

```text
Ваше приложение → Adal Outbound → URL получателя
```

Outbound работает независимо от входящего потока Adal. Для него не нужно создавать `Server`, `Request` или `Destination`: приложение работает с отдельным API и само указывает получателя каждого сообщения.

> **Beta:** Adal Outbound развивается. Перед production-запуском проверьте интеграцию на тестовом получателе и предусмотрите обработку повторной доставки.

## Когда использовать Outbound {#when-to-use}

Outbound подходит приложениям, которые отправляют вебхуки или другие HTTP-события во внешние системы и не хотят самостоятельно поддерживать очередь доставки, планировщик повторных попыток и журнал результатов.

Например:

- SaaS-продукт отправляет события на webhook URL клиентов;
- платформа уведомляет партнёров об изменениях заказов, платежей или других объектов;
- внутренний сервис вызывает публичный API CRM, help desk или другой внешней системы;
- команде нужна единая история исходящих доставок и данные для диагностики;
- для разных потоков необходимо явно выбирать регион отправки и хранения истории.

Один запрос к Outbound создаёт одно сообщение для одного получателя.

## Как работает Outbound {#how-it-works}

Рабочий поток состоит из пяти этапов:

1. Создайте **refresh token** в разделе **Outbound → Tokens**.
2. Обменяйте refresh token через Control Plane на **access token**.
3. Получите список регионов, выберите регион и сохраните его домен.
4. Отправляйте сообщения непосредственно в выбранный регион.
5. После истечения access token получите новый с помощью того же refresh token.

```text
Outbound → Tokens
      │
      ▼
Refresh token
      │
      ▼
POST /auth/outbound
      │
      ▼
Access token
      │
      ├──► GET /outbound/servers
      │
      └──► POST https://{region-domain}/api/send
```

Список регионов не нужно запрашивать при каждом обновлении access token. Получите его при первоначальной настройке, сохраните выбранный `domain` и обновляйте список отдельно, когда хотите проверить появление новых регионов или изменить географию отправки.

Adal не выбирает регион автоматически и не выполняет межрегиональный failover. Регион определяет ваше приложение.

## Аутентификация {#authentication}

Outbound использует два типа токенов.

| Токен | Для чего нужен | Где использовать |
| --- | --- | --- |
| **Refresh token** | Получение access token | Только в Control Plane |
| **Access token** | Получение списка регионов и отправка сообщений | В Control Plane и выбранном регионе |

### Refresh token {#refresh-token}

Refresh token — долгоживущий секрет приложения. Создайте его в разделе **Outbound → Tokens** в дашборде.

Значение refresh token показывается только при создании. Сохраните его сразу: позднее посмотреть или восстановить это значение нельзя. Если токен потерян, создайте новый.

Неиспользуемый или скомпрометированный refresh token можно отозвать в том же разделе. После отзыва получить с его помощью новый access token невозможно.

> Храните refresh token в секретном хранилище приложения. Не помещайте его в исходный код, клиентское приложение, обычные логи или сообщения об ошибках.

### Получение access token {#access-token}

Перед работой с Outbound API обменяйте refresh token на короткоживущий access token:

```http
POST https://cp.adal.cloud/auth/outbound
Authorization: Bearer <refresh-token>
```

Успешный ответ содержит access token и сведения о сроке его действия:

```json
{
  "access_token": "<access-token>",
  "expires_at": 1786200278343
}
```

Access token используется для получения списка регионов и отправки сообщений.

После истечения access token повторите запрос к `/auth/outbound` с тем же действующим refresh token. Повторно получать список регионов только из-за обновления access token не требуется.

Не передавайте refresh token региональным API и не используйте его для отправки сообщений.

## Выбор региона {#choose-region}

Получите список доступных регионов через Control Plane:

```http
GET https://cp.adal.cloud/outbound/servers
Authorization: Bearer <access-token>
```

Пример элемента списка:

```json
{
  "key": "kz2",
  "name": "Almaty, Kazakhstan",
  "domain": "kz2.adal.cloud",
  "country": "Kazakhstan",
  "city": "Almaty"
}
```

| Поле | Описание |
| --- | --- |
| `key` | Идентификатор региона |
| `name` | Отображаемое название |
| `domain` | Домен регионального API |
| `country` | Страна размещения |
| `city` | Город размещения |

Для отправки используйте значение `domain`, возвращённое API. Не формируйте домен региона самостоятельно.

Например:

```text
https://kz2.adal.cloud
```

Если выбранный регион недоступен, Adal не переключит сообщение на другой регион автоматически. Решение о смене региона и последствиях для размещения данных принимает ваше приложение.

## Отправка сообщения {#send-message}

Отправьте сообщение непосредственно в выбранный регион:

```http
POST https://kz2.adal.cloud/api/send
Authorization: Bearer <access-token>
Content-Type: application/json
```

Пример тела запроса:

```json
{
  "destination": "https://example.com/webhooks",
  "method": "POST",
  "headers": {
    "Content-Type": ["application/json"],
    "X-Event-Id": ["evt_01K2..."]
  },
  "body_base64": "eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQifQ==",
  "max_attempts": 3,
  "add_idempotency": true
}
```

### Поля сообщения {#message-fields}

| Поле | Обязательно | Описание |
| --- | :---: | --- |
| `destination` | да | Публичный HTTP- или HTTPS-адрес получателя |
| `method` | да | HTTP-метод исходящего запроса |
| `headers` | нет | Заголовки исходящего запроса |
| `body_base64` | нет | Тело исходящего запроса в Base64 |
| `max_attempts` | нет | Максимальное количество попыток доставки для сообщения |
| `add_idempotency` | нет | Нужно ли Adal генерировать `Idempotency-Key`, если он не передан; значение по умолчанию — `true` |

`destination` должен указывать на одного получателя. Если одно событие необходимо отправить нескольким системам, создайте отдельное сообщение для каждой из них.

### Заголовки {#headers}

Каждому имени заголовка соответствует массив значений:

```json
{
  "X-Event-Id": ["evt_01K2..."],
  "X-Example": ["one", "two"]
}
```

Такой формат позволяет передавать несколько значений одного HTTP-заголовка.

Некоторыми транспортными заголовками управляет Adal. Не передавайте вручную `Host`, `Content-Length`, `Connection`, `Transfer-Encoding` и другие hop-by-hop или прокси-заголовки.

Если API отклоняет заголовок как зарезервированный, удалите его из `headers`.

Можно передать собственный стабильный идентификатор события — например, `X-Event-Id` или `Idempotency-Key`. Adal сохраняет пользовательские заголовки между повторными попытками.

### Автоматический Idempotency-Key {#automatic-idempotency-key}

По умолчанию Adal добавляет автоматически сгенерированный `Idempotency-Key` в исходящий HTTP-запрос, если пользователь не передал собственный ключ. Для управления этим поведением используйте опциональное поле `add_idempotency`:

| Поле | Тип | Обязательное | Значение по умолчанию |
| --- | --- | :---: | :---: |
| `add_idempotency` | boolean | нет | `true` |

Значением должен быть JSON boolean `true` или `false`. Строки и числа, например `"true"`, `1` и `0`, не допускаются.

| `add_idempotency` | Пользовательский `Idempotency-Key` | Результат |
| --- | --- | --- |
| не передан | отсутствует | Adal генерирует ключ в формате UUIDv7 |
| не передан | присутствует | Adal сохраняет пользовательский ключ |
| `true` | отсутствует | Adal генерирует ключ в формате UUIDv7 |
| `true` | присутствует | Adal сохраняет пользовательский ключ |
| `false` | отсутствует | Заголовок не добавляется |
| `false` | присутствует | Adal сохраняет пользовательский ключ |

Значение `false` отключает только автоматическую генерацию. Adal никогда не заменяет и не удаляет пользовательский `Idempotency-Key`.

Автоматически созданный ключ имеет формат UUIDv7:

```http
Idempotency-Key: 01989d55-20df-7a72-87f9-6e50f3d78315
```

Adal создаёт ключ и сохраняет его вместе с сообщением до постановки доставки в очередь. Все попытки доставки одного сообщения, включая повторные, используют одно и то же значение. Новый самостоятельный вызов `/send` без пользовательского ключа получает новый UUIDv7.

Чтобы использовать собственный логический ключ, передайте его в `headers`:

```json
{
  "destination": "https://example.com/hook",
  "method": "POST",
  "headers": {
    "Content-Type": ["application/json"],
    "Idempotency-Key": ["order-create-12345"]
  },
  "body_base64": "eyJvcmRlcl9pZCI6MTIzNDV9",
  "add_idempotency": true
}
```

В исходящий запрос попадёт неизменённый `Idempotency-Key: order-create-12345`. Это полезно, если вызывающему приложению нужно использовать один логический ключ в нескольких вызовах `/send`. Тот же пользовательский ключ сохраняется, даже если `add_idempotency` имеет значение `false`.

Чтобы отключить автоматическую генерацию при отсутствии ключа:

```json
{
  "destination": "https://example.com/hook",
  "method": "POST",
  "add_idempotency": false
}
```

В этом случае Adal не добавляет `Idempotency-Key`.

### Тело запроса {#request-body}

`body_base64` содержит байты тела исходящего HTTP-запроса, закодированные в Base64. Это позволяет передавать JSON, текстовые и бинарные данные в одном формате.

Например, тело:

```json
{"event":"order.created"}
```

передаётся так:

```json
{
  "body_base64": "eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQifQ=="
}
```

Base64 не задаёт тип содержимого. При необходимости передайте `Content-Type` отдельно в `headers`.

## Ответ на отправку {#send-response}

После успешного приёма сообщения Adal возвращает:

```http
202 Accepted
```

Ответ содержит идентификатор сообщения, выбранный регион и начальный статус `pending`. Например:

```json
{
  "id": 123,
  "region": "kz2",
  "status": "pending"
}
```

`202 Accepted` означает только то, что Adal принял сообщение и поставил его на доставку.

Это не означает, что:

- получатель уже получил запрос;
- получатель вернул успешный HTTP-ответ;
- бизнес-операция внутри системы получателя завершилась успешно.

После принятия Adal выполняет доставку асинхронно и сохраняет результат каждой попытки.

## Статусы сообщения {#statuses}

В истории Outbound используются следующие основные состояния:

| Статус | Значение |
| --- | --- |
| `pending` | Сообщение ожидает первой или следующей попытки |
| `delivering` | Adal выполняет HTTP-запрос к получателю |
| `success` | Получатель вернул HTTP-статус класса `2xx` |
| `failed` | Разрешённые попытки исчерпаны без ответа `2xx` |

Эти статусы относятся только к Outbound и не связаны со статусами доставки во входящем потоке Adal.

## Доставка и повторные попытки {#delivery-retries}

Доставка считается успешной, если получатель вернул HTTP-статус класса `2xx`.

Ошибка соединения, DNS или TLS, тайм-аут либо любой другой HTTP-статус считаются неудачной попыткой. Пока лимит `max_attempts` не исчерпан, Adal планирует следующую попытку.

Задержка после неудачной попытки с номером `N` рассчитывается по формуле:

```text
delay = N² minutes
```

Например:

| Неудачная попытка | Задержка до следующей попытки |
| ---: | ---: |
| 1 | 1 минута |
| 2 | 4 минуты |
| 3 | 9 минут |

Следующая попытка выполняется только тогда, когда заданный для сообщения лимит допускает ещё одну попытку.

Автоматические повторные попытки не расходуют дополнительные кредиты. Кредиты списываются один раз, когда Adal принимает сообщение к доставке.

## Модель at least once и идемпотентность {#at-least-once}

Outbound использует модель доставки **at least once**. При некоторых сбоях один и тот же запрос может быть доставлен получателю больше одного раза.

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

Поэтому обработчик получателя должен:

- быть идемпотентным там, где это возможно;
- распознавать повторы по стабильному идентификатору события или idempotency key;
- сохранять результат обработки этого идентификатора;
- не создавать повторный платёж, заказ или уведомление при повторной доставке.

Adal сохраняет переданные заголовки и тело сообщения между попытками, поэтому идентификатор события остаётся тем же. Если Adal генерирует `Idempotency-Key`, одно и то же созданное значение также используется во всех попытках доставки сообщения. Это помогает получателю распознать повторную доставку, но для предотвращения повторного выполнения бизнес-операции получатель всё равно должен реализовать идемпотентную обработку или дедупликацию по ключу.

> Ответ `2xx` означает, что получатель успешно ответил на HTTP-запрос. Он не доказывает, что бизнес-операция внутри системы получателя завершилась успешно.

## Безопасность адреса получателя {#destination-security}

Outbound выполняет запросы по URL, который передаёт пользователь, поэтому Adal проверяет `destination` и защищает инфраструктуру от SSRF.

Поддерживаются только публично доступные HTTP- и HTTPS-адреса. Отклоняются:

- `localhost` и loopback-адреса;
- приватные и служебные диапазоны IP;
- внутренние hostname;
- URL со встроенным логином или паролем.

Адрес проверяется перед подключением и повторно при HTTP redirect. Поэтому Outbound нельзя использовать для прямой отправки на `127.0.0.1`, private IP или внутренний hostname.

Примеры недопустимых адресов:

```text
http://localhost/
http://127.0.0.1/
http://192.168.1.10/
```

Защита Adal не заменяет проверку входящих данных на стороне получателя. Получатель должен проверять подпись или другой способ аутентификации отправителя, валидировать содержимое и ограничивать полномочия используемых учётных данных.

## История и диагностика {#history}

Текущее состояние сообщения и история попыток доступны в дашборде:

```text
Outbound → Messages
```

Сначала выберите регион, через который было отправлено сообщение.

Для каждой попытки, в зависимости от результата, Adal показывает:

- HTTP-код и заголовки ответа;
- текст технической ошибки;
- время DNS-разрешения;
- время установления соединения и TLS;
- TTFB и общее время запроса;
- сведения о TLS-соединении;
- цепочку HTTP redirects и итоговый URL.

Эти данные помогают отличить ошибку DNS от проблемы TLS, тайм-аута, медленного ответа приложения или неуспешного HTTP-статуса.

Тело HTTP-ответа получателя Adal не сохраняет и не показывает.

История сообщений и попыток доступна через дашборд, а не через публичный API.

## Региональная обработка и хранение {#regional-processing}

Сообщение отправляется непосредственно через выбранный регион. Там же хранятся данные, необходимые для доставки и диагностики:

- URL получателя;
- HTTP-метод;
- заголовки;
- тело сообщения;
- текущее состояние;
- история попыток.

Control Plane используется для аутентификации и получения списка регионов, но не является централизованным хранилищем истории Outbound.

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

Сам по себе выбор региона не подтверждает соответствие конкретному закону или отраслевому стандарту. Такая оценка зависит от состава данных, договоров и применимых требований.

Adal не выполняет автоматический failover между регионами. Если приложение переключается на другой регион, учитывайте, что история старых и новых сообщений будет находиться в разных регионах.

## Кредиты {#credits}

Кредиты списываются при успешном принятии сообщения Adal, а не при каждой попытке доставки.

Повторные попытки, которые Adal выполняет для уже принятого сообщения, не требуют дополнительных кредитов.

Автоматически созданный `Idempotency-Key` считается служебным заголовком. Он не учитывается в `max_header_count`, `max_headers_bytes`, при проверке максимального размера запроса и при расчёте количества списываемых кредитов. Поэтому автоматическая генерация не может увеличить стоимость сообщения или привести к превышению пользовательского лимита заголовков.

Если `Idempotency-Key` передал пользователь, он считается обычным пользовательским заголовком и учитывается в лимитах заголовков, при проверке размера запроса и при расчёте кредитов.

Если доступного баланса недостаточно, сообщение не принимается к доставке и ответ `202 Accepted` не возвращается. Проверяйте ответ API до того, как считать сообщение поставленным в очередь.

Актуальные правила расчёта стоимости, баланс и ограничения плана смотрите в дашборде. Не фиксируйте эти значения в коде интеграции.

## Ошибки и устранение неполадок {#troubleshooting}

Считайте сообщение принятым только после ответа `202 Accepted`. Любой другой ответ означает, что постановка в очередь не подтверждена.

Основные причины ошибок:

- неверный, истёкший или отозванный токен;
- истёкший access token;
- ошибка формата запроса;
- неподдерживаемый HTTP-метод;
- запрещённый HTTP-заголовок;
- недопустимый или заблокированный SSRF-защитой `destination`;
- недостаточный баланс;
- превышение rate limit.

| Проблема | Что проверить |
| --- | --- |
| Не удаётся получить access token | Refresh token существует и не был отозван |
| Access token истёк | Повторите `/auth/outbound` с действующим refresh token |
| Refresh token потерян или отозван | Создайте новый в **Outbound → Tokens** |
| Не удаётся отправить сообщение в регион | Используйте актуальный `domain`, полученный из списка регионов |
| API отклоняет заголовок | Удалите зарезервированный или hop-by-hop заголовок |
| `destination` отклонён | Убедитесь, что это публичный HTTP- или HTTPS-адрес без встроенных credentials |
| Сообщение долго остаётся в `pending` | Откройте историю попыток в нужном регионе |
| Сообщение получило `failed` | Изучите техническую ошибку, HTTP-статусы и timings каждой попытки |
| Сообщение не видно в дашборде | Выберите тот же регион, через который выполнялась отправка |

Не записывайте токены, полный URL получателя, заголовки или тело сообщения в обычные логи и системы аналитики: они могут содержать персональные данные и секреты.

## Ограничения {#limitations}

В текущем потоке Outbound:

- регион выбирает приложение, автоматического выбора и межрегионального failover нет;
- одно сообщение предназначено для одного `destination`;
- получатель должен быть публично доступен по HTTP или HTTPS;
- тело HTTP-ответа получателя не сохраняется;
- история сообщений и попыток доступна в дашборде, но не через публичный API.

Outbound не обеспечивает exactly-once delivery, строгий порядок доставки или успешное выполнение бизнес-операции у получателя.

## Полный поток интеграции {#complete-flow}

### 1. Создайте refresh token в дашборде и получите с его помощью access token

```http
POST https://cp.adal.cloud/auth/outbound
Authorization: Bearer <refresh-token>
```

Refresh token сохраните при создании в дашборде. Полученный access token используйте для API-запросов.

### 2. Получите список регионов

```http
GET https://cp.adal.cloud/outbound/servers
Authorization: Bearer <access-token>
```

Выберите регион и сохраните возвращённый `domain` в конфигурации приложения.

### 3. Отправьте сообщение

```http
POST https://kz2.adal.cloud/api/send
Authorization: Bearer <access-token>
Content-Type: application/json
```

```json
{
  "destination": "https://example.com/webhooks",
  "method": "POST",
  "headers": {
    "Content-Type": ["application/json"],
    "X-Event-Id": ["evt_01K2..."]
  },
  "body_base64": "eyJldmVudCI6Im9yZGVyLmNyZWF0ZWQifQ==",
  "max_attempts": 3,
  "add_idempotency": true
}
```

Считайте сообщение принятым только после ответа `202 Accepted`. Сохраните его идентификатор для сопоставления с историей в дашборде.

### 4. Обновляйте access token

Когда access token истечёт, получите новый с помощью refresh token. Не запрашивайте список регионов повторно только из-за обновления токена.

### 5. Проверьте результат доставки

Откройте **Outbound → Messages**, выберите регион отправки и проверьте состояние сообщения и историю попыток.

## Краткая схема API {#api-summary}

| Method | Endpoint | Авторизация   | Назначение                       |
| --- | --- |---------------|----------------------------------|
| `POST` | `https://cp.adal.cloud/auth/outbound` | Refresh token | Получить или обновить access token |
| `GET` | `https://cp.adal.cloud/outbound/servers` | Access token  | Получить список регионов         |
| `POST` | `https://{region-domain}/api/send` | Access token  | Поставить сообщение на доставку  |

## Связанные страницы {#related-pages}

- [Получатели](/docs/destinations) — доставка входящих Requests в настроенные Destinations
- [Повторные попытки](/docs/retries) — повторные попытки во входящем потоке
- [Основные понятия](/docs/concepts) — основные сущности и архитектура Adal
- [Хранение данных](/docs/storage) — правила хранения входящих Requests
