샌드고 API 오류의 대부분은 **재시도로 해결되지 않습니다.** 요청이 잘못됐거나 계정 상태가 문제이기 때문입니다. 이 구분을 먼저 잡아야 무의미한 재시도 루프와 크레딧 낭비를 피할 수 있습니다.

## 응답 형태

```json
{
  "code": "INVALID_TEMPLATE_CODE",
  "message": "존재하지 않는 템플릿 코드입니다."
}
```

검증 실패는 필드별 오류가 함께 옵니다.

```json
{
  "code": "VALIDATION_FAILED",
  "message": "The given data was invalid.",
  "errors": {
    "targetUrl": ["The target url field must be a valid URL."]
  }
}
```

## 오류 코드 전체

| HTTP | 코드 | 뜻 | 재시도 |
| --- | --- | --- | --- |
| 400 | `EMPTY_CONTACTS` | 수신자 배열이 비었다 | ❌ |
| 400 | `INVALID_TEMPLATE_CODE` | 없거나 미승인 템플릿 | ❌ |
| 400 | `VALIDATION_FAILED` | 입력값 검증 실패 | ❌ |
| 400 | `NOT_A_BRAND_MESSAGE` | 자유형(FT/FI/FW)+개별 수신자를 브랜드메시지로 보냄 | ❌ |
| 401 | `INVALID_ACCESS_KEY` | 액세스 키/시크릿이 틀림 | ❌ |
| 402 | `PAYMENT_REQUIRED` | 크레딧 부족 | ❌ (충전 필요) |
| 403 | `ACCESS_KEY_NOT_APPROVED` | 앱이 승인되지 않음 | ❌ |
| 403 | `IP_NOT_ALLOWED` | 허용 IP 밖에서 호출 | ❌ |
| 404 | `NOT_FOUND` | 캠페인 등 대상 없음 | ❌ |
| 404 | `INVALID_KAKAO_SENDER_KEY` | 카카오 발신프로필 키가 틀림 | ❌ |
| — | 타임아웃 / 5xx | 일시적 장애 | ✅ |

**재시도할 가치가 있는 건 마지막 한 줄뿐입니다.**

## 언어별 처리

### Node.js / TypeScript

```typescript
import Sendgo, { SendgoError } from '@sendgo/node';

try {
  await sendgo.alimtalk.send({ templateCode: 'ORDER_CONFIRM_001', contacts });
} catch (error) {
  if (error instanceof SendgoError) {
    switch (error.code) {
      case 'PAYMENT_REQUIRED':
        // 재시도해도 소용없다. 운영자에게 알린다.
        await notifyOps('샌드고 크레딧이 소진되었습니다');
        break;
      case 'INVALID_TEMPLATE_CODE':
      case 'INVALID_KAKAO_SENDER_KEY':
        // 설정 오류 — 배포된 코드가 잘못됐다는 뜻이므로 크게 알린다.
        logger.error('샌드고 설정 오류', { code: error.code });
        break;
      default:
        logger.warn('알림톡 발송 실패', { code: error.code, message: error.message });
    }
    return;   // 재시도하지 않는다
  }

  throw error;  // 네트워크 오류 등은 상위 재시도 로직으로
}
```

### Python

```python
from sendgo import SendgoError

NON_RETRYABLE = {
    "EMPTY_CONTACTS", "INVALID_TEMPLATE_CODE", "VALIDATION_FAILED",
    "INVALID_ACCESS_KEY", "PAYMENT_REQUIRED",
    "ACCESS_KEY_NOT_APPROVED", "IP_NOT_ALLOWED", "INVALID_KAKAO_SENDER_KEY",
}

try:
    client.alimtalk.send(template_code="ORDER_CONFIRM_001", contacts=contacts)
except SendgoError as e:
    if e.code in NON_RETRYABLE:
        logger.error("알림톡 발송 실패(재시도 불가): %s", e.code)
        return
    raise   # 일시적 오류만 상위로 올려 재시도
```

### PHP · Laravel

```php
<?php

use Sendgo\Php\Exception\SendgoException;

try {
    $sendgo->alimtalk->send([
        'templateCode' => config('sendgo.templates.order_confirm'),
        'contacts'     => $contacts,
    ]);
} catch (SendgoException $e) {
    // 발송 실패가 주문 처리를 되돌리게 하지 않는다.
    Log::error('알림톡 발송 실패', [
        'code'    => $e->getCode(),
        'message' => $e->getMessage(),
        'order'   => $order->id,
    ]);
}
```

## 큐에서의 재시도

Laravel 큐나 Celery 를 쓴다면 **재시도 횟수를 낮게 잡으세요.** 기본값을 그대로 두면 재시도로 해결되지 않는 오류에 대해 수십 번 같은 요청을 보냅니다.

```php
<?php

class SendAlimtalk implements ShouldQueue
{
    public int $tries = 3;
    public int $backoff = 30;

    // 재시도해도 소용없는 예외는 즉시 실패 처리한다.
    public function retryUntil(): \DateTime
    {
        return now()->addMinutes(5);
    }
}
```

크레딧 부족(`PAYMENT_REQUIRED`)에서는 특히 조심해야 합니다. 큐에 1만 건이 쌓인 상태에서 잔액이 떨어지면, 재시도 설정에 따라 수만 번의 실패 호출이 발생합니다.

## 요청은 성공, 메시지는 미도달

HTTP 200 은 **접수 성공**이지 도달 성공이 아닙니다.

- 수신자가 카카오톡을 안 쓴다 → [SMS 대체 발송](/ko/cookbook/sms-fallback)으로 커버
- 채널을 차단했다 → 대체 발송으로 커버
- 없는 번호다 → 데이터 정합성 문제. 발송 전 번호 검증 필요

건별 결과는 콘솔의 발송 내역에서 확인합니다.

## 발송 전 점검

가장 좋은 오류 처리는 발송 전에 막는 것입니다.

```php
<?php

// 대량 발송 전 잔액 확인 — 1만 건 도중에 떨어지는 것보다 낫다.
// 번호 정규화 — 하이픈과 국가번호 제거
$contacts = collect($recipients)
    ->map(fn ($r) => preg_replace('/\D/', '', $r->phone))
    ->map(fn ($p) => str_starts_with($p, '82') ? '0'.substr($p, 2) : $p)
    ->unique()                         // 중복 제거 — 두 번 나가고 두 번 과금된다
    ->filter(fn ($p) => strlen($p) >= 10)
    ->values();
```

## 다음 단계

- [SMS 대체 발송](/ko/cookbook/sms-fallback)
- [대량 발송과 치환 변수](/ko/cookbook/bulk-send)