Most Sendgo errors **are not fixed by retrying.** The request is wrong, or the account is. Getting that distinction right is what keeps you from burning credit in a retry loop.

## Response shape

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

Validation failures carry per-field detail:

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

## Every code

| HTTP | Code | Meaning | Retry |
| --- | --- | --- | --- |
| 400 | `EMPTY_CONTACTS` | Recipient array is empty | ❌ |
| 400 | `INVALID_TEMPLATE_CODE` | Unknown or unapproved template | ❌ |
| 400 | `VALIDATION_FAILED` | Input validation failed | ❌ |
| 400 | `NOT_A_BRAND_MESSAGE` | Free-form type sent to the brand-message endpoint | ❌ |
| 401 | `INVALID_ACCESS_KEY` | Wrong access key or secret | ❌ |
| 402 | `PAYMENT_REQUIRED` | Out of credit | ❌ (top up) |
| 403 | `ACCESS_KEY_NOT_APPROVED` | App not approved | ❌ |
| 403 | `IP_NOT_ALLOWED` | Called from outside the allowlist | ❌ |
| 404 | `NOT_FOUND` | Campaign or resource missing | ❌ |
| 404 | `INVALID_KAKAO_SENDER_KEY` | Wrong Kakao sender profile key | ❌ |
| — | Timeout / 5xx | Transient | ✅ |

**Only the last row deserves a retry.**

## Handling it

### 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('Sendgo credit exhausted');
        break;
      case 'INVALID_TEMPLATE_CODE':
      case 'INVALID_KAKAO_SENDER_KEY':
        // Configuration error — the deployed code is wrong. Make it loud.
        logger.error('Sendgo misconfiguration', { code: error.code });
        break;
      default:
        logger.warn('Alimtalk send failed', { code: error.code, message: error.message });
    }
    return;   // do not retry
  }

  throw error;  // network-level problems go to the caller's retry logic
}
```

### 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("Alimtalk send failed, not retryable: %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('Alimtalk send failed', [
        'code'    => $e->getCode(),
        'message' => $e->getMessage(),
        'order'   => $order->id,
    ]);
}
```

## Retries in a queue

Keep the retry count **low**. Framework defaults will happily send the same doomed request dozens of times.

```php
<?php

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

`PAYMENT_REQUIRED` is the dangerous one: with 10,000 jobs queued and a zero balance, default retry settings turn into tens of thousands of failing calls.

## Accepted is not delivered

HTTP 200 means the request was accepted.

- Recipient does not use KakaoTalk → cover it with SMS fallback
- Recipient blocked the channel → same
- Number does not exist → a data problem; validate before sending

Per-recipient outcomes are in the console's send history.

## Prevent rather than handle

```php
<?php

// Normalise numbers, strip country codes, drop duplicates before sending.
$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()                          // duplicates send twice and bill twice
    ->filter(fn ($p) => strlen($p) >= 10)
    ->values();
```

## Next

- [Send a Kakao Alimtalk](/en/cookbook/send-alimtalk)
- [Send SMS, LMS and MMS](/en/cookbook/send-sms)