연동 가이드 · KCP 허브 SMS
다른 서비스에서 이 서버로 문자를 보내는 방법입니다. HTTP 호출만 하면 되고, KCP 연동·발신번호 등록·한글 인코딩은 이쪽에서 처리합니다.
이 두 가지를 모르면 고객에게 문자가 두 번 갑니다.
502 응답 중 status: "unknown" 은 발송 여부를 알 수 없는 상태입니다. 자동 재시도 로직에서 반드시 제외하세요.refId 에는 중복 방지 기능이 없습니다. 같은 값으로 두 번 보내면 두 건이 발송됩니다.키는 발급 시 한 번만 표시됩니다. 잃어버리면 재발급받아야 합니다. 유출되면 곧바로 발송 비용으로 이어지므로 관리자에게 알려 비활성화하세요.
모든 요청에 헤더를 넣습니다.
Authorization: Bearer sms_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
헤더가 없거나, 키가 틀렸거나, 관리자가 키를 비활성화했으면 401 입니다.
| 필드 | 필수 | 설명 |
|---|---|---|
to | 필수 | 수신번호. 하이픈·공백은 알아서 제거합니다. 010 · 011 · 016 · 017 · 018 · 019 |
content | 필수 | 본문. EUC-KR 기준 1~80바이트 (→ 7번) |
from | — | 발신번호. 생략하면 서버 기본 발신번호를 씁니다. 지정하려면 KCP에 사전등록된 번호여야 합니다 |
refId | — | 호출한 쪽에서 쓰는 참조값. 64자 이하. 응답과 조회에 그대로 돌려줍니다 |
{
"message": {
"id": 12,
"status": "success",
"to": "01012345678",
"from": "0212345678",
"content": "[예약안내] 9월 16일 14시에 뵙겠습니다.",
"refId": "booking-1234",
"resCd": "0000",
"resMsg": "정상처리",
"messageId": "352531",
"jubsuNo": "260916386532",
"createdAt": "2026-09-16T05:00:00.000Z",
"completedAt": "2026-09-16T05:00:01.230Z"
}
}
시간은 UTC ISO 8601 입니다. 화면에 표시할 때 현지 시간대로 바꾸세요.
이 절이 이 문서에서 가장 중요합니다. 재발송 판단을 잘못하면 고객에게 문자가 두 번 갑니다.
| HTTP | message.status | 뜻 | 재발송 |
|---|---|---|---|
| 201 | success | 접수 완료 (resCd=0000) | 불필요 |
| 400 | — | 입력 검증 실패 | 안전 |
| 401 | — | 키 문제 | 안전 |
| 429 | — | 한도 초과 | 안전 |
| 502 | failed | KCP가 거절 (미발송 확정) | 안전 |
| 502 | unknown | 발송 여부 불명 | 그냥 재시도 금지 |
400 · 401 · 429 와 502 failed 는 KCP에 요청이 나가기 전이거나 거절이 확정된 경우라 문자가 나가지 않았습니다.
unknown 은 타임아웃이나 통신 중 예외라 이미 발송됐을 수 있습니다. 이때는
message.id 를 기록해 두고GET /api/v1/messages/{id} 로 상태가 바뀌었는지 확인하거나재발송 여부를 정하세요. error 문구가 아니라 message.status 값으로 분기해야 합니다.
502 응답 형태
{
"error": "발송 여부를 확인할 수 없습니다.",
"message": { "id": 13, "status": "unknown", "resCd": "BR90", "resMsg": "...", "...": "..." }
}
GET /api/v1/messages?page=1&pageSize=20&status=success&to=01012345678
| 파라미터 | 기본값 | 설명 |
|---|---|---|
page | 1 | |
pageSize | 20 | 최대 100 |
status | 전체 | pending · success · failed · unknown |
to | 전체 | 수신번호 부분 일치 (하이픈·공백 무시) |
{
"messages": [ { "id": 12, "status": "success", "...": "..." } ],
"page": 1, "pageSize": 20, "total": 1, "totalPages": 1
}
GET /api/v1/messages/12
없으면 404, id 가 정수가 아니면 400 입니다.
자기 API 키로 보낸 건만 조회됩니다. 다른 서비스가 보낸 건이나 관리 화면에서 보낸 건은 보이지 않습니다.
| HTTP | 본문 | 원인 |
|---|---|---|
400 | { error } | JSON 파싱 실패, 번호 형식 오류, 본문 길이·문자 오류 |
401 | { error } | 키 없음·오류·비활성 |
404 | { error } | 조회 대상 없음 |
429 | { error } | 한도 초과 |
502 | { error, message } | KCP 거절 또는 발송 불명 |
resCd 가 BR 로 시작하면 KCP가 아니라 이쪽 브리지가 낸 오류입니다.
| resCd | 뜻 | 발송 |
|---|---|---|
| BR01 | 필수값 누락 | 미발송 |
| BR02 | EUC-KR로 표현할 수 없는 문자 | 미발송 |
| BR03 | 본문 80바이트 초과 | 미발송 |
| BR90 | 브리지 실행 실패·타임아웃 | 불명 |
| BR99 | 브리지 예외 | 불명 |
그 외 4자리 코드는 KCP가 돌려준 값이며 resMsg 에 사유가 들어 있습니다.
본문은 EUC-KR 기준 80바이트까지입니다. 영문·숫자·기본 기호가 1바이트, 한글·한자·전각 기호가 2바이트입니다. 즉 한글만 쓰면 40자, 영문만 쓰면 80자가 상한입니다.
JavaScript의 str.length 로 재면 안 됩니다. 한글이 1로 세어져 통과시킨 뒤 서버에서 400 을 맞습니다. 아래에서 직접 확인해 보세요.
이모지(😀), 힣 · 똠 같은 KS X 1001 밖의 한글, 일부 특수문자는 EUC-KR로 인코딩할 수 없어 거절됩니다. 400 응답에 어떤 문자가 문제인지 함께 옵니다.
{ "error": "EUC-KR로 보낼 수 없는 문자가 있습니다: 😀 (이모지, 일부 특수문자·한글은 지원하지 않습니다)" }
사용자 입력을 그대로 넣는 경우라면 보내기 전에 걸러내거나, 400 을 받아 사용자에게 되돌려 주세요.
| 대상 | 기본값 |
|---|---|
| API 키 1개당 분당 | 60건 |
| API 키 1개당 하루 | 2,000건 |
초과하면 429 와 함께 Retry-After(재시도까지 남은 초) · RateLimit-Limit · RateLimit-Remaining · RateLimit-Reset 헤더가 옵니다. Retry-After 값만큼 기다렸다 재시도하면 됩니다.
검증 실패(400)도 한도를 소비합니다. 한도 검사가 본문 검증보다 먼저 돌기 때문이며, 잘못된 요청을 무한히 던지는 것을 막기 위한 의도된 동작입니다. 형식 검증은 호출하는 쪽에서 먼저 하세요.
한도 카운터는 서버 프로세스 메모리에 있습니다. 서버가 재배포·재시작되면 초기화됩니다.
curl
curl -X POST https://sms.koax.site/api/v1/messages \
-H "Authorization: Bearer $SMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"01012345678","content":"[안내] 접수되었습니다.","refId":"order-9001"}'
Node.js
async function sendSms({ to, content, refId }) {
const res = await fetch('https://sms.koax.site/api/v1/messages', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SMS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ to, content, refId }),
});
const body = await res.json();
if (res.status === 201) return body.message;
if (res.status === 429) {
const after = Number(res.headers.get('Retry-After') || 60);
throw new Error(`한도 초과. ${after}초 뒤 재시도`);
}
// 발송 여부 불명 — 재시도하면 중복 발송될 수 있다. 호출부가 판단하도록 구분해 던진다.
if (res.status === 502 && body.message?.status === 'unknown') {
const err = new Error(`발송 여부 불명 (이력 #${body.message.id})`);
err.indeterminate = true;
err.messageId = body.message.id;
throw err;
}
throw new Error(body.error || `발송 실패 (HTTP ${res.status})`);
}
PHP
function send_sms(string $to, string $content, ?string $refId = null): array {
$ch = curl_init('https://sms.koax.site/api/v1/messages');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('SMS_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode(
['to' => $to, 'content' => $content, 'refId' => $refId],
JSON_UNESCAPED_UNICODE
),
CURLOPT_TIMEOUT => 40, // 브리지 타임아웃(30초)보다 길게 잡는다
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$body = json_decode($raw, true);
if ($status === 201) {
return $body['message'];
}
if ($status === 502 && ($body['message']['status'] ?? '') === 'unknown') {
// 이미 발송됐을 수 있다. 그냥 재시도하지 말 것.
throw new RuntimeException('발송 여부 불명: 이력 #' . $body['message']['id']);
}
throw new RuntimeException($body['error'] ?? "발송 실패 (HTTP {$status})");
}
본문은 UTF-8로 보내세요. EUC-KR 변환은 서버가 합니다.
502 unknown 을 자동 재시도에서 제외했는가str.length 아님)GET /api/v1/messages/{id} 로 조회하세요.