> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aieev.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 외부 채널

> Slack·Discord·웹훅으로 알림 보내기

콘솔 알림을 팀이 쓰는 Slack·Discord 채널이나 직접 운영하는 서버(웹훅)로 보낼 수 있습니다. 채널마다 받을 알림을 고르고, [로그 알림](https://docs.aieev.com/docs/ko/air-container/log-alerts.md) 규칙에서 알림을 보낼 채널을 지정할 수도 있습니다.

## 지원하는 채널

| 종류 | 주소 | 비고 |
| :- | :- | :- |
| **Slack** | `https://hooks.slack.com/services/…` | Slack [Incoming Webhook](https://api.slack.com/messaging/webhooks) 주소 |
| **Discord** | `https://discord.com/api/webhooks/…` | Discord 채널의 [웹훅](https://support.discord.com/hc/articles/228383668) 주소 |
| **웹훅** | 인터넷에서 접근할 수 있는 `https://` 주소 | [Standard Webhooks](https://www.standardwebhooks.com) 방식으로 서명해 보냅니다. [웹훅 받기](#웹훅-받기)를 참고하세요. |

* 모든 채널 주소는 `https://`여야 합니다.
* 조직당 채널은 10개까지 연결할 수 있습니다. 꺼진 채널도 개수에 포함됩니다.
* 저장한 주소는 다시 전체를 보여 주지 않고 앞뒤 일부만 표시합니다.

## 채널 연결하기

1. 콘솔에서 **알림**을 열고 **외부 채널** 탭을 선택합니다. 조직 설정의 **외부 채널** 카드에서도 들어갈 수 있습니다.
2. **채널 연결**을 누릅니다.
3. 채널 주소를 붙여 넣습니다. Slack·Discord 주소는 종류가 자동으로 선택됩니다. 주소를 만드는 방법은 입력 칸 옆의 **주소 만드는 법**에서 확인할 수 있습니다.
4. **보낼 알림**과 옵션을 고릅니다.
5. **테스트 메시지 보내기**로 채널에 메시지가 오는지 확인한 뒤 저장합니다.

**별칭**은 콘솔에서 채널을 구분하는 이름입니다. 비워 두면 Slack·Discord는 종류 이름으로, 웹훅은 주소의 호스트 이름으로 정해집니다.

웹훅 채널을 만들면 **웹훅 서명 키**가 한 번만 표시됩니다. 받는 서버에 저장해 두세요. 키를 복사해야 창을 닫을 수 있습니다.

## 보낼 알림 고르기

| 선택지 | 보내는 알림 |
| :- | :- |
| **장애·경고만** (기본, 추천) | 컨테이너 배포 실패, 컨테이너 장애, 임대 자원 만료 임박, 저잔액 경고, 크레딧 만료 임박, 크레딧 소진으로 컨테이너 차단 |
| **전체** | 보낼 수 있는 모든 알림. 조직 소유자가 아니면 결제·청구 알림은 빠집니다. |
| **직접 고르기** | 영역별로 원하는 알림만 고릅니다. |

> [!NOTE]
>
> 채널에는 고른 알림 목록이 그대로 저장됩니다. 나중에 새 알림 종류가 생겨도 기존 채널에는 자동으로 추가되지 않습니다.

### 보낼 수 있는 알림

| 영역 | 알림 |
| :- | :- |
| **컨테이너** | 배포 완료, 배포 실패, 장애, 오토스케일링 작동, 대기 신청 GPU 확보, 레플리카 한도 증가 승인 |
| **임대** | 임대 완료, 임대 자원 사용 시작, 만료 임박, 만료 |
| **크레딧** | 저잔액 경고, 크레딧 만료 임박, 크레딧 소진으로 컨테이너 차단 |
| **결제·청구** (소유자만 선택) | 크레딧 충전, 자동 충전 완료, 정기 결제 충전 완료, 결제 실패, 자동 충전 실패, 결제 환불, 인보이스 연체 |
| **멤버** | 멤버 참여, 멤버 퇴장 |

다음 알림은 외부 채널로 보내지 않습니다.

* **로그 알림**: 채널 구독과 관계없이, 규칙에서 고른 채널로만 보냅니다.
* **나에게만 오는 알림**: 요청 접수·처리 결과, 내 의견에 대한 답변.
* **서비스 공지**와 **외부 채널 연동 꺼짐** 알림.

### 특정 프로젝트만 보내기

**특정 프로젝트의 컨테이너 알림만 보내기**를 켜면 고른 프로젝트의 컨테이너 알림만 보냅니다. 크레딧·임대·멤버 알림은 프로젝트와 관계없이 보냅니다.

## 메시지 옵션

* **메시지 언어**: 한국어 또는 English를 고릅니다. 기본값은 조직 지역(미국이면 English, 그 외는 한국어)이며, 채널마다 저장되므로 나중에 조직 지역을 바꿔도 기존 채널의 언어는 그대로입니다. 메시지의 시각은 한국어면 KST, 영어면 UTC로 표시됩니다.
* **장애 알림에 @here 멘션 붙이기**: Slack·Discord 채널에서 장애(빨간색) 알림에 `@here`를 붙입니다. 기본은 꺼져 있습니다.
* **로그 줄과 배포 실패 사유 포함**: 로그 알림의 맞은 로그와 배포 실패 원인을 메시지에 함께 보냅니다. 기본은 켜져 있습니다. 끄면 제목과 콘솔 링크만 보내므로, 로그에 민감한 값이 찍힐 수 있다면 꺼 두세요.

## 메시지 모양

Slack과 Discord에는 알림 한 건이 카드 하나로 도착합니다.

* 카드 색은 알림의 심각도를 나타냅니다 — 장애(빨강), 경고(주황), 완료(초록), 안내(회색).
* 제목과 내용 아래에 로그 알림의 맞은 로그(최대 5줄)나 배포 실패 원인이 코드 블록으로 붙습니다.
* **컨테이너 보기**, **로그 보기**, **결제 보기**처럼 콘솔의 해당 화면으로 가는 링크가 붙습니다.
* 맨 아래에 `AirCloud · {조직 이름}`이 표시됩니다.

## 웹훅 받기

### 요청

AirCloud는 알림마다 채널 주소로 `POST` 요청을 보냅니다.

| 헤더 | 값 |
| :- | :- |
| `Content-Type` | `application/json` |
| `User-Agent` | `AirCloud-Notifications/1.0` |
| `webhook-id` | 전송 ID. 자동 재시도와 수동 다시 보내기에서도 같습니다. |
| `webhook-timestamp` | 보낸 시각(Unix 초). 시도할 때마다 바뀝니다. |
| `webhook-signature` | `v1,<서명>` 형식의 서명 |

### 본문

```json theme={null}
{
  "id": "6f1c2b0e-2d4a-4a57-9c3e-0b8f0d6a1e42",
  "type": "endpoint.deploy_failed",
  "version": 1,
  "created_at": "2026-11-03T05:12:33.123456+00:00",
  "severity": "error",
  "organization": { "id": "org-xxxxxxxxxxxx", "name": "Acme" },
  "title": "컨테이너 'my-llm' 배포에 실패했습니다",
  "text": null,
  "url": "https://…/console/projects/…/container/…",
  "data": { "endpoint_id": "…", "endpoint_name": "my-llm", "project_id": "…" },
  "detail": "RuntimeError: …"
}
```

| 필드 | 설명 |
| :- | :- |
| `id` | 알림 ID. 같은 알림을 여러 채널로 보내면 모든 채널에서 같습니다. |
| `type` | 알림 종류. 예: `endpoint.deploy_failed`, `endpoint.log_alert`, `balance.low_balance`. 테스트 메시지는 `integration.test`입니다. |
| `version` | 본문 형식 버전. 지금은 항상 `1`입니다. |
| `created_at` | 알림이 생긴 시각(ISO 8601, UTC) |
| `severity` | `error`, `warning`, `success`, `info` 중 하나 |
| `organization` | 조직 ID와 이름 |
| `title`, `text` | 채널의 메시지 언어로 쓴 제목과 내용. 내용이 없으면 `text`는 `null`입니다. |
| `url` | 콘솔의 해당 화면 주소. 없으면 `null`입니다. |
| `data` | 알림별 추가 정보. 알림 종류마다 필드가 다르며, 형식이 바뀔 수 있으니 `title`·`text`·`url`을 우선 쓰는 것이 좋습니다. |
| `detail` | 배포 실패·장애 알림의 원래 실패 사유. 그 밖의 알림이나 **로그 줄과 배포 실패 사유 포함**을 끈 채널에서는 `null`입니다. |

### 서명 검증

웹훅은 [Standard Webhooks](https://www.standardwebhooks.com) 규격으로 서명합니다. 서명은 `{webhook-id}.{webhook-timestamp}.{본문}`을 서명 키로 HMAC-SHA256 한 값을 base64로 인코딩하고 앞에 `v1,`을 붙인 것입니다. 서명 키는 `whsec_` 뒤의 base64 문자열을 풀어 씁니다.

* 본문은 **받은 그대로의 바이트**로 검증하세요. JSON을 파싱했다가 다시 직렬화하면 서명이 맞지 않습니다.
* `webhook-timestamp`가 너무 오래된 요청(예: 5분 이상)은 버려 재전송 공격을 막으세요.
* 언어별 [공식 라이브러리](https://github.com/standard-webhooks/standard-webhooks)를 쓰면 검증을 직접 구현하지 않아도 됩니다.

```python Python theme={null}
import base64
import hashlib
import hmac
import time

TOLERANCE_SECONDS = 5 * 60


def verify(secret: str, headers: dict, body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    signature = headers["webhook-signature"]

    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    for item in signature.split(" "):
        version, _, value = item.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return True
    return False
```

> [!NOTE]
>
> 저장하기 전에 연결 창에서 보내는 테스트 메시지는 아직 서명 키가 없어 `webhook-signature` 헤더 없이 갑니다. 저장한 채널에서 보내는 테스트 메시지는 서명됩니다.

### 응답과 재시도

* **성공**: 2xx로 응답하면 전송된 것으로 봅니다. **10초** 안에 응답해야 하며, 리다이렉트는 따라가지 않습니다.
* **재시도**: 응답이 없거나 실패하면 30초, 2분, 10분, 30분, 1시간, 2시간 간격으로 다시 보냅니다(처음 시도 포함 최대 7번, 약 4시간). 처음 만든 지 6시간이 지난 전송은 더 보내지 않습니다.
* **Retry-After**: 응답에 `Retry-After` 헤더가 있으면 그만큼 기다렸다가 보냅니다(최대 30분).
* **중복 거르기**: 같은 알림이 두 번 이상 도착할 수 있습니다. `webhook-id`로 중복을 거르세요.

| 응답 | Slack·Discord | 웹훅 |
| :- | :- | :- |
| 2xx | 전송됨 | 전송됨 |
| 429, 5xx, 시간 초과, 연결 실패 | 다시 시도 | 다시 시도 |
| 그 밖의 4xx | 다시 시도하지 않고 실패 | 다시 시도 |
| 채널 삭제·권한 없음 (아래 표) | 채널을 끔 | 410이면 채널을 끔 |

## 전송 기록과 다시 보내기

채널을 누르면 상세 화면에서 켜기/끄기, 테스트 메시지, 최근 전송 기록을 볼 수 있습니다.

* **최근 전송 기록**: 최근 50건을 보여 주며, 기록은 30일 동안 보관합니다. 상태는 **대기**, **전송됨**, **실패**, **건너뜀** 중 하나입니다.
* **다시 보내기**: 실패한 전송만 다시 보낼 수 있습니다. 다시 보내면 재시도 주기가 처음부터 시작되며, `webhook-id`는 그대로입니다.
* **테스트 메시지**는 전송 기록에 남지 않고, `@here` 멘션도 붙지 않습니다.

## 채널이 자동으로 꺼지는 경우

보낼 수 없는 상태가 확인되면 AirCloud가 채널을 끕니다.

| 상황 | 해결 방법 |
| :- | :- |
| Slack·Discord 채널이나 웹훅이 삭제됨 (Slack 404·410, Discord 404, 웹훅 410) | 새 주소로 바꾼 뒤 다시 켭니다. |
| 채널에 글을 쓸 권한이 없음 (Slack·Discord 401·403) | 웹훅을 다시 만들어 주소를 바꾼 뒤 다시 켭니다. |
| 주소가 내부망 주소로 풀림 | 인터넷에서 접근할 수 있는 주소로 바꿉니다. |
| 하루 넘게 전송에 실패함 | 받는 쪽을 확인한 뒤 다시 켭니다. |

* 채널이 꺼지면 조직 전체에 **외부 채널 연동 꺼짐** 알림이 가고, 조직 소유자에게 메일이 발송됩니다.
* 꺼지는 시점에 대기 중이던 전송은 **건너뜀**이 됩니다. 꺼져 있는 동안 생긴 알림은 나중에 다시 켜도 보내지 않습니다.
* 다시 켜려면 채널 상세 화면의 스위치를 켭니다. 주소를 바꾸는 것만으로는 채널이 켜지지 않습니다.
* 직접 끄거나 삭제한 경우에는 알림을 보내지 않습니다.

## 서명 키 다시 만들기

웹훅 채널 상세 화면의 **서명 키 다시 만들기**로 새 키를 받을 수 있습니다. **지금 키는 바로 무효가 되며** 유예 기간이 없습니다. 받는 서버에 새 키를 넣기 전까지 서명 검증이 실패하므로, 새 키를 바로 반영하세요. 대기 중인 재시도도 새 키로 서명됩니다.

## 권한

| 작업 | 멤버 | 소유자 |
| :- | :-: | :-: |
| 채널 연결·수정·켜기/끄기·연결 끊기, 서명 키 다시 만들기, 테스트 메시지, 전송 기록 보기 | ✅ | ✅ |
| 로그 알림 규칙에 채널 붙이기 | ✅ | ✅ |
| 실패한 전송 다시 보내기 (결제·청구 알림 제외) | ✅ | ✅ |
| 결제·청구 알림 고르기, 결제·청구 알림 다시 보내기 | — | ✅ |
| 결제·청구 알림을 받는 채널의 주소 변경·끄기·삭제·서명 키 다시 만들기 | — | ✅ |

## 주소 제한

* `https://` 주소만 쓸 수 있고, 주소에 사용자 이름·비밀번호를 넣을 수 없습니다.
* `localhost`, `.local`, `.internal` 같은 내부 이름과 사설·루프백·링크 로컬 IP 주소는 쓸 수 없습니다.
* 보낼 때마다 주소를 다시 확인해, 내부망 주소로 풀리면 보내지 않고 채널을 끕니다.

## 연결 끊기

채널 상세 화면의 **연결 끊기**로 채널을 지웁니다. 전송 기록도 함께 지워지고, 그 채널을 고른 로그 알림 규칙에서도 빠집니다.

## 관련 문서

> [card] [알림](https://docs.aieev.com/docs/ko/console/notifications.md)
>
> 받을 수 있는 알림과 콘솔·데스크톱·이메일 경로를 설정합니다.

> [card] [로그 알림](https://docs.aieev.com/docs/ko/air-container/log-alerts.md)
>
> 컨테이너 로그에 조건을 걸어 외부 채널로 알림을 보냅니다.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.