Sendgo API error codes and retry strategy
Every error code returned by the Sendgo send endpoints, and how to tell a failure worth retrying from one that will fail identically every time.
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
{
"code": "INVALID_TEMPLATE_CODE",
"message": "존재하지 않는 템플릿 코드입니다."
}
Validation failures carry per-field detail:
{
"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
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
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
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
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
// 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
자주 묻는 질문
- Should I retry a failed send?
- Usually not. Almost every Sendgo failure is either a malformed request (template code, sender key, recipients) or an account state problem (unapproved app, no credit), and the same request will fail the same way. Only network timeouts and 5xx responses are worth retrying.
- How do I handle PAYMENT_REQUIRED?
- It means the credit balance is exhausted. Retrying does not help — alert an operator and top up. During a bulk send every remaining message will fail, so check the balance before large batches.
- Why do I only get IP_NOT_ALLOWED locally?
- The app has an IP allowlist that does not include your development machine. Either use a separate development app with no allowlist, or add your IP. For production, note that behind a NAT gateway or load balancer the outbound IP differs from the instance IP.
- The request succeeded but no message arrived.
- Acceptance and delivery are different things. The recipient may not use KakaoTalk or may have blocked the channel. Check per-recipient results in the console, and enable SMS fallback for notifications that must arrive.