
# Adal Outbound

Adal Outbound is a dedicated service for outbound HTTP delivery. Your application provides the recipient URL, HTTP method, headers, and request body. Adal queues the message, delivers it, retries failed attempts, and records the results.

```text
Your application → Adal Outbound → Recipient URL
```

Outbound operates independently of Adal's inbound webhook flow. You do not need to create a `Server`, `Request`, or `Destination`: your application uses a separate API and specifies the recipient for each message.

> **Beta:** Adal Outbound is under active development. Before using it in production, test your integration with a test recipient and make sure it can handle duplicate deliveries.

## When to use Outbound {#when-to-use}

Outbound is designed for applications that send webhooks or other HTTP events to external systems without maintaining their own delivery queue, retry scheduler, and delivery log.

For example:

- a SaaS product sends events to customer webhook URLs;
- a platform notifies partners about changes to orders, payments, or other resources;
- an internal service calls the public API of a CRM, help desk, or another external system;
- a team needs a single delivery history with diagnostic data;
- different workloads need an explicit choice of delivery region and history storage location.

Each request to Outbound creates one message for one recipient.

## How Outbound works {#how-it-works}

The workflow consists of five steps:

1. Create a **refresh token** under **Outbound → Tokens**.
2. Exchange the refresh token for an **access token** through the Control Plane.
3. Retrieve the list of regions, select one, and save its domain.
4. Send messages directly to the selected region.
5. When the access token expires, obtain a new one using the same refresh token.

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

You do not need to retrieve the region list every time you renew an access token. Retrieve it during initial setup, save the selected `domain`, and refresh the list separately when you want to check for new regions or change the delivery location.

Adal does not select a region automatically or perform cross-region failover. Your application determines which region to use.

## Authentication {#authentication}

Outbound uses two types of tokens.

| Token | Purpose | Where to use it |
| --- | --- | --- |
| **Refresh token** | Obtain an access token | Control Plane only |
| **Access token** | Retrieve regions and send messages | Control Plane and the selected region |

### Refresh token {#refresh-token}

A refresh token is a long-lived application secret. Create one under **Outbound → Tokens** in the dashboard.

The refresh token is displayed only when it is created. Save it immediately: you cannot view or recover its value later. If you lose the token, create a new one.

You can revoke an unused or compromised refresh token from the same dashboard section. Once revoked, the token can no longer be used to obtain an access token.

> Store the refresh token in your application's secret storage. Do not include it in source code, client-side applications, ordinary logs, or error messages.

### Obtaining an access token {#access-token}

Before using the Outbound API, exchange the refresh token for a short-lived access token:

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

A successful response contains the access token and its expiration details:

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

Use the access token to retrieve the region list and send messages.

When the access token expires, repeat the request to `/auth/outbound` with the same valid refresh token. You do not need to retrieve the region list again simply because the access token was renewed.

Do not send the refresh token to regional APIs or use it to send messages.

## Selecting a region {#choose-region}

Retrieve the list of available regions through the Control Plane:

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

Example list item:

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

| Field | Description |
| --- | --- |
| `key` | Region identifier |
| `name` | Display name |
| `domain` | Regional API domain |
| `country` | Host country |
| `city` | Host city |

Use the `domain` value returned by the API when sending messages. Do not construct the regional domain yourself.

For example:

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

If the selected region is unavailable, Adal does not automatically move the message to another region. Your application is responsible for deciding whether to change regions and accounting for the data-location implications.

## Sending a message {#send-message}

Send the message directly to the selected region:

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

Example request body:

```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 {#message-fields}

| Field | Required | Description |
| --- | :---: | --- |
| `destination` | yes | Public HTTP or HTTPS recipient URL |
| `method` | yes | HTTP method for the outgoing request |
| `headers` | no | Outgoing request headers |
| `body_base64` | no | Base64-encoded outgoing request body |
| `max_attempts` | no | Maximum number of delivery attempts for the message |
| `add_idempotency` | no | Whether Adal should generate an `Idempotency-Key` when one is not supplied; defaults to `true` |

Each `destination` must identify a single recipient. If you need to send the same event to multiple systems, create a separate message for each recipient.

### Headers {#headers}

Each header name maps to an array of values:

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

This format allows you to provide multiple values for the same HTTP header.

Adal manages certain transport headers. Do not set `Host`, `Content-Length`, `Connection`, `Transfer-Encoding`, or other hop-by-hop or proxy headers manually.

If the API rejects a header as reserved, remove it from `headers`.

You may include your own stable event identifier, such as `X-Event-Id` or `Idempotency-Key`. Adal preserves user-supplied headers across retry attempts.

### Automatic Idempotency-Key {#automatic-idempotency-key}

By default, Adal adds an automatically generated `Idempotency-Key` to the outgoing HTTP request when you do not provide one. Use the optional `add_idempotency` field to control this behavior:

| Field | Type | Required | Default |
| --- | --- | :---: | :---: |
| `add_idempotency` | boolean | no | `true` |

The value must be the JSON boolean `true` or `false`. Strings and numbers such as `"true"`, `1`, and `0` are not accepted.

| `add_idempotency` | User-supplied `Idempotency-Key` | Result |
| --- | --- | --- |
| omitted | absent | Adal generates a UUIDv7 key |
| omitted | present | Adal keeps the user-supplied key |
| `true` | absent | Adal generates a UUIDv7 key |
| `true` | present | Adal keeps the user-supplied key |
| `false` | absent | The header is not added |
| `false` | present | Adal keeps the user-supplied key |

`false` disables only automatic generation. Adal never replaces or removes a user-supplied `Idempotency-Key`.

An automatically generated key uses the UUIDv7 format:

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

Adal generates the key and stores it with the message before queueing the delivery. Every attempt for that message uses the same value, including retries. A separate call to `/send` without a user-supplied key receives a new UUIDv7.

To use your own logical key, include it in `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
}
```

The outgoing request contains `Idempotency-Key: order-create-12345` unchanged. This is useful when the calling application needs to reuse one logical key across multiple `/send` calls. The same user-supplied key is preserved even when `add_idempotency` is `false`.

To disable automatic generation when no key is supplied:

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

In this case, Adal does not add an `Idempotency-Key`.

### Request body {#request-body}

`body_base64` contains the outgoing HTTP request body encoded as Base64. This allows JSON, text, and binary data to use the same message format.

For example, this body:

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

is sent as:

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

Base64 does not specify a media type. If required, provide `Content-Type` separately in `headers`.

## Send response {#send-response}

After accepting the message, Adal returns:

```http
202 Accepted
```

The response contains the message ID, selected region, and initial `pending` status. For example:

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

`202 Accepted` only means that Adal accepted the message and queued it for delivery.

It does not mean that:

- the recipient has already received the request;
- the recipient returned a successful HTTP response;
- the recipient's business operation completed successfully.

After accepting the message, Adal processes it asynchronously and records the result of each delivery attempt.

## Message statuses {#statuses}

Outbound delivery history uses the following primary statuses:

| Status | Meaning |
| --- | --- |
| `pending` | The message is waiting for its first or next attempt |
| `delivering` | Adal is sending the HTTP request to the recipient |
| `success` | The recipient returned a `2xx` HTTP status |
| `failed` | All permitted attempts were exhausted without a `2xx` response |

These statuses apply only to Outbound and are unrelated to delivery statuses in Adal's inbound flow.

## Delivery and retries {#delivery-retries}

Delivery is considered successful when the recipient returns a `2xx` HTTP status.

A connection, DNS, or TLS error, a timeout, or any other HTTP status counts as a failed attempt. As long as the `max_attempts` limit has not been reached, Adal schedules another attempt.

The delay after failed attempt number `N` is calculated as follows:

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

For example:

| Failed attempt | Delay before the next attempt |
| ---: | ---: |
| 1 | 1 minute |
| 2 | 4 minutes |
| 3 | 9 minutes |

A subsequent attempt is made only when the limit configured for the message permits another attempt.

Automatic retries do not consume additional credits. Credits are charged once, when Adal accepts the message for delivery.

## At-least-once delivery and idempotency {#at-least-once}

Outbound uses an **at-least-once** delivery model. Under some failure conditions, the same request may be delivered to the recipient more than once.

For example, the recipient may process the request successfully, but its response may be lost in transit. Adal cannot reliably distinguish that situation from one where the request was not processed, so it performs another attempt.

The recipient should therefore:

- process requests idempotently where possible;
- identify duplicates using a stable event ID or idempotency key;
- store the processing result for that identifier;
- avoid creating a duplicate payment, order, or notification when a request is delivered again.

Adal preserves the supplied headers and message body across attempts, so the event identifier remains unchanged. When Adal generates an `Idempotency-Key`, it also uses the same generated value for every attempt of the message. This helps the recipient recognize a retry, but the recipient must still implement idempotency or deduplication for the key to prevent a repeated business operation.

> A `2xx` response means that the recipient successfully responded to the HTTP request. It does not prove that the recipient's internal business operation completed successfully.

## Recipient URL security {#destination-security}

Outbound sends requests to a user-provided URL, so Adal validates `destination` and protects its infrastructure against SSRF.

Only publicly accessible HTTP and HTTPS addresses are supported. Adal rejects:

- `localhost` and loopback addresses;
- private and reserved IP ranges;
- internal hostnames;
- URLs containing embedded usernames or passwords.

The address is validated before the connection is opened and again after an HTTP redirect. As a result, Outbound cannot deliver requests directly to `127.0.0.1`, a private IP address, or an internal hostname.

Examples of invalid addresses:

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

Adal's protections do not replace input validation on the recipient side. The recipient must verify the sender's signature or other authentication mechanism, validate the request data, and limit the permissions of any credentials it uses.

## History and diagnostics {#history}

The current message status and delivery-attempt history are available in the dashboard:

```text
Outbound → Messages
```

First select the region through which the message was sent.

Depending on the result, Adal displays the following information for each attempt:

- HTTP response status and headers;
- technical error details;
- DNS resolution time;
- connection and TLS setup time;
- TTFB and total request time;
- TLS connection details;
- the HTTP redirect chain and final URL.

This data helps distinguish a DNS failure from a TLS problem, timeout, slow application response, or unsuccessful HTTP status.

Adal does not store or display the recipient's HTTP response body.

Message and attempt history is available through the dashboard, not through the public API.

## Regional processing and storage {#regional-processing}

Each message is sent directly through the selected region. The data required for delivery and diagnostics is stored in the same region, including:

- recipient URL;
- HTTP method;
- headers;
- message body;
- current status;
- attempt history.

The Control Plane is used for authentication and region discovery, but it is not a centralized store for Outbound history.

Region selection may matter for network routing, processing location, and your organization's internal data-location requirements.

Selecting a region alone does not establish compliance with a particular law or industry standard. That assessment depends on the data involved, applicable agreements, and relevant requirements.

Adal does not perform automatic cross-region failover. If your application switches to another region, the history of older and newer messages will be stored in different regions.

## Credits {#credits}

Credits are charged when Adal successfully accepts a message, not for each delivery attempt.

Retries performed for an already accepted message do not consume additional credits.

An automatically generated `Idempotency-Key` is a service header. It is not included in `max_header_count`, `max_headers_bytes`, maximum request-size validation, or the credit calculation. Automatic generation therefore cannot increase the message cost or cause the message to exceed user header limits.

A user-supplied `Idempotency-Key` is treated like any other user header and is included in header limits, request-size validation, and the credit calculation.

If the available balance is insufficient, the message is not accepted for delivery and Adal does not return `202 Accepted`. Check the API response before treating the message as queued.

Refer to the dashboard for current pricing calculations, balance, and plan limits. Do not hard-code these values into your integration.

## Errors and troubleshooting {#troubleshooting}

Treat a message as accepted only after receiving `202 Accepted`. Any other response means that the message has not been confirmed as queued.

Common causes of errors include:

- an invalid, expired, or revoked token;
- an expired access token;
- an invalid request format;
- an unsupported HTTP method;
- a prohibited HTTP header;
- an invalid `destination` or one blocked by SSRF protection;
- insufficient balance;
- a rate limit being exceeded.

| Problem | What to check |
| --- | --- |
| Cannot obtain an access token | Make sure the refresh token exists and has not been revoked |
| Access token has expired | Repeat `/auth/outbound` with a valid refresh token |
| Refresh token has been lost or revoked | Create a new token under **Outbound → Tokens** |
| Cannot send a message through a region | Use the current `domain` returned by the region list |
| API rejects a header | Remove the reserved or hop-by-hop header |
| `destination` is rejected | Make sure it is a public HTTP or HTTPS URL without embedded credentials |
| Message remains `pending` for a long time | Open the attempt history in the correct region |
| Message has a `failed` status | Review the technical error, HTTP statuses, and timings for each attempt |
| Message does not appear in the dashboard | Select the same region through which the message was sent |

Do not write tokens, the complete recipient URL, headers, or message body to ordinary logs or analytics systems: they may contain personal data or secrets.

## Limitations {#limitations}

In the current Outbound workflow:

- the application selects the region; there is no automatic selection or cross-region failover;
- each message has one `destination`;
- the recipient must be publicly accessible over HTTP or HTTPS;
- the recipient's HTTP response body is not stored;
- message and attempt history is available in the dashboard, but not through the public API.

Outbound does not provide exactly-once delivery, strict delivery ordering, or a guarantee that the recipient's business operation completed successfully.

## Complete integration flow {#complete-flow}

### 1. Create a refresh token in the dashboard and use it to obtain an access token

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

Save the refresh token when you create it in the dashboard. Use the returned access token for API requests.

### 2. Retrieve the region list

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

Select a region and save the returned `domain` in your application's configuration.

### 3. Send a 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
}
```

Treat the message as accepted only after receiving `202 Accepted`. Save its ID so you can match it to the history shown in the dashboard.

### 4. Renew the access token

When the access token expires, obtain a new one using the refresh token. Do not retrieve the region list again solely because the token was renewed.

### 5. Check the delivery result

Open **Outbound → Messages**, select the delivery region, and review the message status and attempt history.

## API summary {#api-summary}

| Method | Endpoint | Authorization | Purpose |
| --- | --- | --- | --- |
| `POST` | `https://cp.adal.cloud/auth/outbound` | Refresh token | Obtain or renew an access token |
| `GET` | `https://cp.adal.cloud/outbound/servers` | Access token | Retrieve the region list |
| `POST` | `https://{region-domain}/api/send` | Access token | Queue a message for delivery |

## Related pages {#related-pages}

- [Destinations](/docs/destinations) — deliver inbound Requests to configured Destinations
- [Retries](/docs/retries) — retry behavior for the inbound flow
- [Core concepts](/docs/concepts) — Adal's core entities and architecture
- [Data storage](/docs/storage) — storage rules for inbound Requests
