SMS / LMS / MMS / 이메일 API
HTTP POST 요청으로 문자·이메일을 발송합니다. 발신번호는 사전에 등록된 번호만 사용 가능합니다.
https://imsend.kr/api/v1
인증
모든 API 요청에 X-API-Key 헤더를 포함해야 합니다.
POST /api/v1/sms/send HTTP/1.1
Host: imsend.kr
Content-Type: application/json
X-API-Key: your_api_key_here
예약·대기 문자도 실행 직전 금칙어·수신거부·야간 정책을 다시 확인합니다. 정책 조회 장애 또는 야간이면 발송을 보류하고 재검사합니다. 수신거부·금칙어는 실패 처리하며 미발송 건은 과금하지 않습니다. 예약 문자 유형 S/SMS, L/LMS, M/MMS를 동일하게 처리합니다.
광고 문자의 수신거부 필터에는 080 수신거부 중 발신자를 확인하지 못한 번호도 포함됩니다. 운영자가 확인할 때까지 모든 발송자의 광고 문자에서 제외되며(optoutFiltered에 집계), 정보성 문자에는 영향이 없습니다. 수신거부 조회 장애 시 광고 발송은 503으로 보류합니다.
REST API 키는 가맹점 관리자에게 발급받습니다. MCP 회원 키와 호환되지 않습니다. 키 원본은 발급 응답에서만 표시하고 서버는 해시만 저장합니다. 분실 시 새 키를 발급해 교체하세요.
※ 2026-09-21 웹 발송 화면(회원·가맹점 포털)이 API와 같은 발송 정책 판정을 쓰도록 정리됐습니다. 요청·응답 형식, 한도, 과금, 재시도 정책은 변경되지 않았습니다(API 영향 없음).
※ 2026-09-24 내부: 원장 쓰기 직렬화(LedgerWriter)·결과 과금 소수 보존 — 요청·응답 변경 없음(/api/v1/point/record 포함).
SMS 단건 발송
POST /api/v1/sms/send
90바이트 이내 단문 문자를 발송합니다. 초과 시 자동으로 LMS로 전환됩니다.
요청 파라미터
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| receiver | string | 필수 | 수신번호 (하이픈 제거, 예: 01012345678) |
| message | string | 필수 | 발송 내용 (최대 90자) |
| callback | string | 선택 | 발신번호(sender 별칭). 승인(APPROVED)된 번호만 그대로 씁니다. 미입력이면 승인 기본번호(없으면 시스템 대표번호), 미승인 번호를 지정하면 거부하지 않고 같은 규칙으로 대체한 뒤 응답 senderWarning(대체된 번호가 요청 번호와 같으면 — 예: 시스템 대표번호 지정 — 싣지 않음) 으로 알립니다. 실제 사용 번호는 응답 sender |
| clientRequestId | string | 선택 | 중복 발송 방지 키. ^[A-Za-z0-9:_.-]{1,100}$, 가맹점 안에서 요청마다 유일해야 합니다. 보내면 수신자 1,000명 이하·전부 아니면 0 적재·같은 키 재호출 시 원 응답 반환이 적용됩니다 — 중복 방지 키 참조 (2026-09-23 추가, 보내지 않으면 기존 동작 그대로) |
| adFormat | string | 선택 | "auto" 면 광고 문자에 서버가 법정 문구((광고) 가맹점명 접두 + 무료수신거부 080번호 접미)를 붙입니다 — 발송 미리보기의 finalText 와 같은 본문. 이미 붙어 있으면 다시 붙이지 않습니다. 시스템 080 번호가 없으면 503 OPTOUT_080_UNSET. 생략하면 기존 동작((광고) 접두만 보장 — 전송자명·080 은 호출측이 붙임) |
요청 예시
{
"receiver": "01012345678",
"message": "안녕하세요! 내일 오전 10시 회의가 있습니다.",
"callback": "0339116510"
}
응답 예시
발송은 배치 단위로 처리됩니다. 단건도 receivers 1건짜리 배치로 큐에 적재되며 동일한 배치 응답을 반환합니다. (top-level status/msgId는 없습니다 — 상태는 /api/v1/sms/logs로 조회)
{
"success": true,
"batchId": "a1b2c3d4",
"totalCount": 1,
"successCount": 1,
"failCount": 0,
"optoutFiltered": 0,
"unitPrice": 12,
"totalCost": 12,
"queued": true
}
💰 단가: 12원/건 (GT Agent 전달 성공 시 차감)
LMS 장문 발송
POST /api/v1/sms/send
2,000자 이내 장문 문자입니다. msgType을 LMS로 지정합니다.
{
"receiver": "01012345678",
"message": "안녕하세요.\n\n이번 행사 안내를 드립니다.\n일시: 2026년 6월 1일 오후 2시\n장소: 춘천 임팩시스 본사\n\n많은 참여 부탁드립니다.",
"msgType": "LMS"
}
⚠️ LMS·MMS 제목(subject)은 지원하지 않습니다 — 보내도 서버가 읽지 않고 제목 없이 발송됩니다. 제목이 필요하면 본문 첫 줄에 넣으세요.
💰 단가: 35원/건
MMS 멀티미디어 발송
POST /api/v1/sms/send
이미지를 포함한 문자 발송입니다. 이미지 전달 방식은 두 가지입니다. ⚠️ 외부 URL은 지원하지 않습니다 — 이미지는 imsend 서버에 저장되어야 첨부됩니다 (JPG/PNG, 최대 2MB, 자동으로 300KB 이하 JPG 변환).
방법 A — Base64 직접 첨부 (1회 호출)
imageBody에 base64 이미지를 담아 발송합니다. data:image/...;base64, 접두사도 허용됩니다.
{
"receiver": "01012345678",
"message": "이번 이벤트를 안내해 드립니다.",
"msgType": "MMS",
"imageBody": "/9j/4AAQSkZJRgABAQ...(base64)"
}
방법 B — 업로드 후 imageId 참조 (대용량·재사용)
이미지를 먼저 업로드해 imageId를 받고, /send에서 참조합니다.
POST /api/v1/sms/image/upload (multipart/form-data, 필드명 file)
curl -X POST https://imsend.kr/api/v1/sms/image/upload \
-H "X-API-Key: YOUR_API_KEY" \
-F "file=@event.jpg"
# 응답: { "success": true, "imageId": "a1b2c3d4.jpg" }
받은 imageId를 발송에 전달합니다.
{
"receiver": "01012345678",
"message": "이번 이벤트를 안내해 드립니다.",
"msgType": "MMS",
"imageId": "a1b2c3d4.jpg"
}
💰 단가: 85원/건 · 이미지 JPG/PNG · 최대 2MB(자동 ≤300KB 변환)
대량 발송
POST /api/v1/sms/send
대량 발송은 별도 엔드포인트가 아닙니다. SMS 발송과 동일한 /api/v1/sms/send에 receiver 대신 receivers 배열을 전달하면 여러 수신자에게 동일한 문자가 한 번에 발송됩니다. 모든 수신자가 하나의 batchId로 큐에 적재됩니다.
{
"receivers": ["01012345678", "01087654321", "01011112222"],
"message": "안내 공지입니다. 내일 시스템 점검이 있습니다.",
"msgType": "SMS",
"callback": "0339116510"
}
응답
단건과 동일한 배치 응답을 반환합니다(SMS 발송 섹션 참조). totalCount/successCount는 수신자 수에 따라 달라집니다.
{
"success": true,
"batchId": "a1b2c3d4",
"totalCount": 3,
"successCount": 3,
"failCount": 0,
"optoutFiltered": 0,
"unitPrice": 12,
"totalCost": 36,
"queued": true
}
중복 방지 키 (clientRequestId)
POST /api/v1/sms/send 본문에 clientRequestId 를 넣으면, 응답을 받지 못해(타임아웃·연결 끊김) 다시 호출해도 문자가 두 번 나가지 않습니다. 2026-09-23 추가. 키를 보내지 않는 기존 호출은 동작·응답이 그대로입니다.
형식·범위
- 형식 ^[A-Za-z0-9:_.-]{1,100}$. 형식이 틀리거나 빈 문자열이면 400 INVALID_CLIENT_REQUEST_ID. JSON null 은 키 없음으로 봅니다.
- 범위는 가맹점(API 키의 가맹점) 단위입니다. 같은 가맹점 안에서 요청마다 새 값을 쓰세요(예: order-20260923-0001:1). 다른 가맹점과는 겹쳐도 됩니다.
- 키가 있는 요청은 수신자(receiver+receivers)가 1,000명 이하여야 합니다. 넘으면 400 TOO_MANY_RECEIVERS — 1,000명 단위로 나눠 조각마다 다른 키를 쓰세요.
- 본문은 비교하지 않습니다. 같은 키로 내용이 다른 요청을 보내면 두 번째 내용은 발송되지 않고 첫 요청의 응답이 돌아옵니다. 키를 재사용하지 마세요.
- 키가 있으면 batchId 는 20자입니다(키가 없으면 기존 8자). 응답에 clientRequestId 가 함께 실립니다.
- 적재는 전부 아니면 0 입니다. 키가 있는 요청은 수신자 전원의 대기열 적재와 완료 기록을 한 트랜잭션으로 처리합니다. 일부만 적재된 상태는 생기지 않습니다. 번호 형식 오류·수신거부로 걸러진 수신자는 기존처럼 failCount·optoutFiltered 로 셉니다.
응답과 재시도 판단
| HTTP | 의미 | 호출자 조치 |
|---|---|---|
| 200 | 첫 처리 응답(적재 완료). 같은 키 재호출이면 저장된 원 응답 그대로 + "idempotentReplay": true | 완료로 기록. batchId 로 결과를 조회 |
| 4xx | 미접수(적재 0). 형식 오류·401·402 잔액 부족·수신자 초과 등 | 원인을 고친 뒤 같은 키로 재시도해도 됩니다(새로 처리) |
| 503 IN_PROGRESS | 같은 키 요청이 처리 중. 헤더 Retry-After: 30, 본문에 batchId | 상태를 "모름"으로 두고 30초 뒤 같은 키로 다시 호출. 선점 후 10분이 지나도 완료 기록이 없으면 적재 0 이 확정이므로, 그 뒤의 같은 키 호출은 같은 batchId 로 새로 발송하고 그 응답을 줍니다 |
| 500 | 서버 오류. 적재 트랜잭션이 롤백됐거나 선점을 확인하지 못함(둘 다 적재 0) | 같은 키로 재시도 |
| 응답 없음 | 타임아웃·연결 끊김 — 적재됐는지 알 수 없음 | 같은 키로 재호출. 적재됐으면 200 재응답, 처리 중이면 503, 아니면 새로 처리 |
// 요청
{
"receivers": ["01000001111", "01000002222"],
"message": "(광고) 가을 할인 안내 ... 무료수신거부 080XXXXXXX",
"adType": "ad",
"clientRequestId": "campaign-42:chunk-1"
}
// 200 (첫 응답 — 평면 JSON)
{
"success": true,
"batchId": "3f9c0a1b2c4d5e6f7a8b",
"sender": "0330000000",
"totalCount": 2, "successCount": 2, "failCount": 0, "optoutFiltered": 0,
"unitPrice": 35, "totalCost": 70, "queued": true,
"clientRequestId": "campaign-42:chunk-1"
}
// 200 (같은 키 재호출 — 위 응답 그대로 + idempotentReplay)
{ ..., "clientRequestId": "campaign-42:chunk-1", "idempotentReplay": true }
// 503 + Retry-After: 30
{ "success": false, "code": "IN_PROGRESS", "message": "같은 clientRequestId 요청을 처리 중입니다. ...", "batchId": "3f9c0a1b2c4d5e6f7a8b" }
// 400
{ "success": false, "message": "clientRequestId 요청은 수신자 1000명 이하", "code": "TOO_MANY_RECEIVERS" }
📌 중복 방지 키는 /api/v1/sms/send 에만 있습니다. 리셀러 대행 발송(/api/admin/send)·MCP 발송에는 아직 없습니다.
발송 미리보기
POST /api/v1/sms/preview
보내기 전에 서버 정책 판정만 받습니다 — 최종 본문(광고 법정 문구 포함)·바이트·유형·야간 시간창·단가·예상 비용·가용 잔액. 발송·차감·거절 기록이 없습니다. 응답은 {success, data} 형식입니다.
요청
| 필드 | 필수 | 설명 |
|---|---|---|
| message | 필수 | 본문 |
| adType | 선택 | "ad" = 광고. 본문이 (광고) 로 시작해도 광고로 봅니다 |
| scheduledAt | 선택 | 보낼 시각(KST, 예: 2026-04-13T09:00). 없으면 지금 기준으로 야간을 판정 |
| receiverCount | 선택 | 수신자 수(1~1,000,000, 기본 1) — estCost 계산용 |
| sender | 선택 | 발신번호 — 실제 쓸 번호를 응답 sender 로 돌려줍니다(/sms/send 와 같은 대체 규칙) |
| subject | 선택 | 받지만 판정에 쓰지 않습니다 |
응답 (data)
| 필드 | 설명 |
|---|---|
| finalText | /sms/send 에 adFormat: "auto" 로 보냈을 때 적재되는 본문과 같습니다 |
| msgType · bytes | SMS/LMS/MMS · finalText 의 EUC-KR 바이트(90 초과면 LMS) |
| isAd | 광고 판정 결과 |
| sendableNow · blockReasons | 막는 사유가 없으면 true. 사유 코드: NIGHT_AD_BLOCKED(광고 21~08시) · FORBIDDEN_KEYWORD · OPTOUT_080_UNSET · SCHEDULE_IN_PAST · INSUFFICIENT_BALANCE(목록은 늘어날 수 있습니다) |
| nextSendableAt | 야간 차단이면 다음 08:00(KST, yyyy-MM-ddTHH:mm:ss), 아니면 null |
| unitPrice · estCost | 가맹점 적용 단가 · 단가 × receiverCount (수신거부 제외분은 반영하지 않습니다) |
| available · walletMode | 잔액 조회와 같은 가용액 · SELF/PARENT |
// 요청
{ "message": "주말 세일 30%", "adType": "ad", "receiverCount": 100 }
// 응답
{ "success": true, "data": {
"msgType": "SMS", "bytes": 55, "isAd": true,
"finalText": "(광고) 가맹점명\n주말 세일 30%\n무료수신거부 080XXXXXXX",
"sendableNow": true, "nextSendableAt": null, "blockReasons": [],
"unitPrice": 12, "estCost": 1200, "available": 50000, "walletMode": "SELF",
"sender": "0212345678" } }
📌 판정은 호출 시점 기준입니다. 실제 발송 시 수신거부·금칙어·시간창을 다시 봅니다.
배치 발송 결과
GET /api/v1/sms/batch/{batchId}/results?page=1&size=1000
한 배치(/sms/send 1회)의 수신자별 결과와 과금액을 줍니다. 2026-09-23 추가. 상태는 등록(예약이면 예약) 시각 + 72시간에 고정되고, 과금액은 + 7일에 확정됩니다. 대사(정산 맞춤)에 쓰도록 만든 API 입니다.
- batchId 형식 ^[A-Za-z0-9-]{1,36}$. page 1 이상(기본 1), size 1 이상(기본 1000, 1000 초과는 1000). 형식 오류는 400(본문 message).
- 목록은 발송 로그 순서입니다. hasMore 가 true 면 다음 page 를 읽습니다. summary 는 페이지와 무관하게 배치 전체 집계입니다.
- 다른 가맹점의 배치·없는 배치는 빈 목록(200)입니다(존재 여부를 드러내지 않습니다).
- ⚠️ receiver 는 수신번호 원문입니다. 받은 쪽에서 로그·화면에 그대로 남기지 마세요. 발송 로그 보관 기간이 곧 조회 가능 기간입니다.
상태 4종
| status | 의미 | failReason |
|---|---|---|
| PENDING | 아직 결과 없음(대기열·발송 중·예약 대기). 72시간 뒤에는 나오지 않습니다 | null |
| DELIVERED | 수신 확인 | null |
| FAILED | 실패로 종결(실패·결과 미수신 타임아웃·취소 포함) | 결과 코드(발송 결과 코드), 없으면 내부 상태(TIMEOUT·CANCELLED 등) |
| UNCONFIRMED | 도착을 확인하지 못함. 72시간 전에는 수기 정리(SENT) 건, 72시간 뒤에는 그때까지 결과가 없거나 결과가 늦게 온 건 | NO_RESULT_72H(72시간 안에 종결 없음) · LATE_FINAL(최초 종결이 72시간 뒤) · 72시간 전 수기 정리 건은 결과 코드 |
72시간 상태 고정 규칙
- 기준 시각 = 예약 시각(예약 발송) 또는 등록 시각. finalizedAt = 기준 + 72시간.
- 최초 종결(DELIVERED·FAILED·TIMEOUT·CANCELLED — 수기 정리 SENT 는 종결이 아님)이 finalizedAt 이내면 그 최초 상태로 고정합니다. 그 뒤 로그가 다시 바뀌어도 API 값은 바뀌지 않습니다.
- 최초 종결이 finalizedAt 뒤면 UNCONFIRMED/LATE_FINAL, 그때까지 종결이 없으면 UNCONFIRMED/NO_RESULT_72H 입니다.
- 따라서 finalizedAt 이 지난 행의 status 는 다시 조회해도 같습니다.
- ⚠️ 이 기능 배포(2026-09-23 ③b) 이전에 종결된 행은 최초 종결 시각이 기록돼 있지 않아 발송 시각(없으면 등록 시각)으로 근사합니다. 배포 이후 발송분은 정확합니다.
과금 (charged · chargedAmount)
- 과금은 상태와 분리해 포인트 원장에서 구합니다. chargedAmount = 그 발송 건의 차감(USE·CREDIT_USED) − 환불(REFUND) 순액, charged = 순액 > 0. 그래서 72시간 뒤 늦게 도착·과금된 건은 UNCONFIRMED 이면서 charged: true 일 수 있습니다.
- chargedAmount 는 소수 2자리 JSON 숫자입니다(단가가 9.5 처럼 소수일 수 있음, 예 28.50). 정수로 자르지 마세요.
- 비용의 정본은 행별 chargedAmount 합계(= summary.chargedAmount)입니다. 한 응답의 목록·요약은 같은 시점의 원장으로 계산되어 항상 일치합니다. DELIVERED 건수 × 단가 나 charged 건수 × 단가 로 계산하지 마세요(대체 발송 단가·부분 환불이 다릅니다).
- costFinalizedAt = 기준 시각 + 7일. 그 전에는 늦은 차감·환불이 반영돼 값이 바뀔 수 있고, 그 뒤에는 바뀌지 않습니다(그 시각까지의 원장만 합산). 그 뒤에 생기는 늦은 과금·환불은 이 API 에 반영하지 않고, imsend 운영자가 가맹점 원장을 정정(조정 기록)하고 통보합니다. summary.costFinalizedAt 은 배치 행 중 가장 늦은 값입니다.
권장 조회 시점
발송 뒤 10분(대부분 도착 확인) → 1시간 → costFinalizedAt(7일) 이후 1회. 마지막 조회 값이 최종입니다. 짧은 간격의 반복 폴링은 하지 마세요.
// 응답
{
"success": true,
"data": {
"list": [
{ "receiver": "01000001111", "status": "DELIVERED", "failReason": null,
"deliveredAt": "2026-09-23T10:00:12", "finalizedAt": "2026-09-26T10:00:00",
"charged": true, "chargedAmount": 35.00, "costFinalizedAt": "2026-09-30T10:00:00" },
{ "receiver": "01000002222", "status": "UNCONFIRMED", "failReason": "NO_RESULT_72H",
"deliveredAt": null, "finalizedAt": "2026-09-26T10:00:00",
"charged": false, "chargedAmount": 0.00, "costFinalizedAt": "2026-09-30T10:00:00" }
],
"summary": { "total": 2, "pending": 0, "delivered": 1, "failed": 0, "unconfirmed": 1,
"chargedCount": 1, "chargedAmount": 35.00, "costFinalizedAt": "2026-09-30T10:00:00" },
"page": 1, "size": 1000, "hasMore": false
}
}
📌 시각은 KST yyyy-MM-dd'T'HH:mm:ss(오프셋 없음). deliveredAt 은 status 가 DELIVERED 일 때만 있고 발송 로그의 발송 시각입니다.
이메일 발송
cc는 선택 이메일 문자열 배열(최대 99개)입니다. 사용 시 to는 유효한 주소 1개여야 합니다.
대소문자를 무시해 중복 주소를 제거하고 한 통의 받는 사람/참조로 발송합니다. 참조 주소는 수신자에게 공개됩니다.
광고 수신거부 주소는 참조에서도 제외하며, 받는 사람이 수신거부면 PRIMARY_RECEIVER_OPTED_OUT으로 중단합니다.
totalCount, successCount, totalCost는 참조까지 포함한 수신자 기준입니다. 참조 없는 기존 다중 수신 발송은 개별 전송을 유지합니다.
{"to":"recipient@example.com","cc":["reviewer@example.com"],"subject":"검토 요청","content":"확인 부탁드립니다."}
광고 이메일은 21시~08시(KST) NIGHT_AD_BLOCKED(400)로 차단합니다.
수신거부 조회 장애 시 OPTOUT_LOOKUP_FAILED(503)로 발송을 중단합니다. 참조 필드는 /email/send 전용입니다.
POST /api/v1/email/send
HTML 이메일을 발송합니다.
{
"to": "user@example.com",
"subject": "이메일 제목",
"content": "<h1>안녕하세요</h1><p>본문 내용입니다.</p>",
"fromName": "임팩시스"
}
💰 단가: 1.2원/건
2026-09-15: 발송 처리 로직을 가맹점 포털과 공유하도록 내부 구조를 바꿨습니다. 요청·응답·상태코드·발송 정책은 바뀌지 않습니다.
📌 from(발신 이메일) 미지정 시 기본 발신주소를 사용합니다. 발신주소는 사전 등록·인증된 주소여야 하며, 미인증 발신은 발송이 거부됩니다.
⚠️ 성공 판정: top-level success:true 여도 successCount:0 이면 실제 미발송입니다. 실패 시 응답 failures[]에 수신자별 사유 {email, reason}가 포함됩니다.
이메일 대량 발송
POST /api/v1/email/send/bulk
recipients는 객체 배열입니다. 각 객체는 email(필수)과 수신자별 치환 변수(예: name)를 담습니다. scheduledAt(선택) 지정 시 예약 발송됩니다.
광고(2026-09-24~): adType:"ad" 이거나 제목·본문이 (광고) 로 시작하면 광고로 봅니다. 21~08시(예약은 예약 시각 기준)면 400 NIGHT_AD_BLOCKED, 수신거부 주소는 빼고(optoutFiltered) 과금하지 않으며, 제목에 (광고) 를 붙입니다. 수신거부 조회 장애면 503 OPTOUT_LOOKUP_FAILED. 예약 배치는 발송 시각에도 다시 확인해 야간이면 08시 이후로 미루고, 그사이 들어온 수신거부도 제외합니다.
{
"recipients": [
{ "email": "a@example.com", "name": "홍길동" },
{ "email": "b@example.com", "name": "김철수" }
],
"subject": "뉴스레터 4월호",
"content": "<h1>안녕하세요</h1><p>4월 소식입니다.</p>",
"scheduledAt": "2026-06-01T09:00:00"
}
// 응답
{
"success": true,
"batchId": "a1b2c3d4",
"totalCount": 2
}
이메일 발송 로그
GET /api/v1/email/logs
이메일만 조회합니다. 파라미터: page(기본 1)·size(기본 20)·status·startDate·endDate(선택). 응답은 data.list에 항목 배열, data.total/page/size 포함.
2026-09-14부터 발송한 이메일은 본문 전체가 보존되어 imsend 발송이력 상세 화면에서 원문 그대로 확인할 수 있습니다. 이전에는 앞부분(1,000자)만 남아 긴 HTML 본문이 깨져 보였습니다. API 요청·응답 형식은 바뀌지 않습니다.
예약 발송 배치 목록
GET /api/v1/email/scheduled
예약/대량 발송 배치 목록을 batches 배열로 반환합니다(최근 100건).
예약 발송 배치 취소
DELETE /api/v1/email/batch/{batchId}
상태가 PENDING인 예약 배치만 취소됩니다. 응답: { "success": true }(취소 대상이 없으면 false).
발신 이메일 인증
이메일 발송 시 from(발신주소)은 사전에 인증된 주소여야 NCP가 발송을 허용합니다. 아래 API로 발신주소를 등록하면 해당 주소로 인증 메일이 발송되며, 메일 내 링크를 클릭하면 인증이 완료됩니다.
인증 흐름 ① register 호출 → ② 발신주소로 인증 메일 도착 → ③ 수신자가 메일의 링크 클릭(imsend 로그인 불필요 — 가맹점/외부 담당자도 바로 인증) → ④ 인증 완료 안내 페이지 표시 → ⑤ list에서 status:VERIFIED 확인 후 발송.
1) 인증 요청 (인증메일 발송)
POST /api/v1/email/sender/register
{
"email": "noreply@mycompany.com",
"displayName": "마이컴퍼니"
}
// 응답
{
"success": true,
"status": "PENDING",
"email": "noreply@mycompany.com",
"expiredAt": "2026-06-01T14:00:00",
"message": "... 24시간 내에 링크를 클릭해 인증을 완료하세요."
}
📌 인증 링크 클릭은 메일 수신자(사람)가 수행합니다(자동화 불가). 이미 인증된 주소면 status:"VERIFIED"로 즉시 반환됩니다. 링크 유효기간 24시간.
2) 발신 이메일 목록/상태 조회
GET /api/v1/email/sender/list
// 응답
{
"success": true,
"count": 1,
"senders": [{
"senderSeq": 12,
"email": "noreply@mycompany.com",
"displayName": "마이컴퍼니",
"status": "VERIFIED",
"verifiedAt": "2026-05-31T14:20:11+09:00",
"regDate": "2026-05-31T14:00:03+09:00"
}]
}
📌 status가 VERIFIED인 주소만 발송 from으로 사용하세요. 시각은 KST(+09:00).
3) 발신 이메일 삭제
DELETE /api/v1/email/sender/{senderSeq}
💡 /api/v1/email/send 호출 시 from이 미인증이면 응답에 senderVerified:false 와 senderHint가 포함되어 원인을 알려줍니다.
발송 이력 조회 (전 채널 통합)
GET /api/v1/sms/logs
| 파라미터 | 타입 | 설명 |
|---|---|---|
| size | integer | 페이지당 건수 (기본: 20) |
| page | integer | 페이지 번호 (기본: 1) |
| startDate | string | 시작일 (YYYY-MM-DD) |
| endDate | string | 종료일 (YYYY-MM-DD) |
| msgType | string | SMS / LMS / MMS / EMAIL (미지정·ALL 시 전체) |
| status | string | 상태 필터 (예: SENT / FAILED, 미지정·ALL 시 전체) |
전 채널 통합(SMS·LMS·MMS·이메일·알림톡) 조회. msgType 미지정 시 전체, status로 상태 필터, 발송시각 내림차순.
GET /api/v1/sms/logs?size=20&page=1&startDate=2026-05-01&msgType=ALL
X-API-Key: your_api_key_here
응답 (list 항목)
{
"success": true,
"data": {
"total": 128, "page": 1, "size": 20,
"list": [{
"channel": "EMAIL",
"receiver": "user@example.com",
"subject": "이벤트 안내",
"message": "...",
"status": "SENT",
"failReason": null,
"cost": 1.2,
"sentAt": "2026-05-31T14:45:02+09:00"
}]
}
}
📌 sentAt은 KST(+09:00). 이메일은 receiver=이메일주소·subject=제목. (기존 phone·msg_type·reg_date 필드도 호환 유지)
잔액 조회
GET /api/v1/sms/balance
잔액과 채널별 단가를 함께 반환합니다. 응답은 data 아래에 중첩되며 단위는 point(포인트, 1point=1원 상당)입니다.
// 응답
{
"success": true,
"data": {
"tenantId": "acme",
"tenantName": "ACME",
"balance": 4980,
"billingType": "PREPAID",
"creditLimit": 0,
"available": 4980,
"unit": "point",
"priceSms": 12,
"priceLms": 35,
"priceMms": 85,
"priceAlimtalk": 10,
"priceEmail": 1.2
}
}
📌 price*는 해당 가맹점에 실제 적용되는 단가입니다(가맹점별 협의 단가가 설정된 경우 그 값, 아니면 기본 단가). 발송 비용 계산·표시는 발송 섹션의 고정 표기값이 아니라 이 응답값을 기준으로 하세요. (AI 에이전트/MCP 발송은 별도 단가 — 요금 안내 참고)
080 수신거부 번호 조회
GET /api/v1/optout/info
광고 문자 하단 무료수신거부 고지에 쓸 시스템 080 번호를 반환합니다. 인증은 다른 가맹점 API 와 같이 X-API-Key(또는 Authorization: Bearer)이며, 키가 없거나 무효면 401입니다. 번호가 설정되지 않았으면 optout080 은 null(200)입니다.
// 응답
{
"success": true,
"data": { "optout080": "080XXXXXXX" }
}
수신거부 변경분 · 스냅샷
080 무료수신거부로 내 가맹점 광고에 대해 수신을 거부한 번호를 연동 서비스가 자기 쪽 수신 동의 상태에 반영하도록 제공합니다. 2026-09-23 추가. 인증은 X-API-Key(또는 Authorization: Bearer), 없거나 무효면 401. imsend 는 이 API 와 무관하게 발송 시점에 수신거부 번호를 이미 걸러냅니다 — 이 API 는 연동 서비스의 자체 기록을 맞추는 용도입니다.
1) 변경 이벤트 피드
GET /api/v1/optout?since={커서}&size=1000
- since(선택): 직전 응답의 nextCursor. 불투명 문자열로 저장만 하고 해석·생성하지 마세요. 없으면 보존 구간의 처음부터 줍니다. 형식이 틀리면 400 INVALID_CURSOR.
- size: 1 이상(기본 1000, 1000 초과는 1000). 0·음수·숫자 아님은 400 INVALID_SIZE.
- list 는 변경 이벤트 순서입니다. 같은 번호가 여러 번 올 수 있으니(거부 → 해제 → 재거부) 받은 순서대로 적용하세요. 번호별 최신 1건으로 줄이면 중간 상태가 사라집니다.
- hasMore 가 true 면 바로 since=nextCursor 로 이어서 읽고, false 면 주기적으로(예: 10분) 다시 부릅니다. 목록이 비어도 nextCursor 는 같은 값으로 돌아오니 그대로 보관하세요.
- 반영이 끝난 뒤에 커서를 저장하세요. 저장 전에 실패하면 같은 커서로 다시 읽어 같은 이벤트를 다시 적용합니다(순서대로 다시 적용해도 결과가 같습니다).
| 필드 | 의미 |
|---|---|
| receiver | 수신번호 원문(숫자). 받은 쪽에서 로그·화면에 그대로 남기지 마세요 |
| status | BLOCKED = 수신 거부. RELEASED = imsend 운영자가 그 거부를 해제함(오등록 정정·본인 철회 확인 등) — 거부 때문에 연동 쪽에서 막아 둔 것을 풀어도 된다는 뜻이지, 수신 동의를 새로 받았다는 뜻은 아닙니다 |
| rejectedAt / releasedAt | 거부 시각 / 해제 시각(KST, 해제 전이면 null) |
| attribution | SINGLE | MULTIPLE. 080 번호는 여러 가맹점이 함께 쓰므로, 거부 직전 7일 안에 이 번호로 광고를 보낸 발송자 모두에게 거부가 귀속됩니다. MULTIPLE 이면 다른 발송자의 광고를 거부했을 수도 있으니, 연동 쪽에서 "보류"로 다루는 등 판단에 쓰세요 |
// 응답
{
"success": true,
"data": {
"list": [
{ "receiver": "01000001111", "status": "BLOCKED", "rejectedAt": "2026-09-23T09:12:00", "releasedAt": null, "attribution": "SINGLE" },
{ "receiver": "01000001111", "status": "RELEASED", "rejectedAt": "2026-09-23T09:12:00", "releasedAt": "2026-09-23T15:40:03", "attribution": "SINGLE" }
],
"nextCursor": "v1.1234567.89",
"hasMore": false
}
}
2) 내보내지 않는 것
- 다른 가맹점·회원·관리자 발송에 귀속된 거부.
- 발신자를 확인하지 못한 080 거부(운영자가 발송자를 배정하기 전) — 그동안 imsend 가 모든 발송자의 광고에서 이 번호를 거릅니다.
- 전체 차단(ALL·SYSTEM) — imsend 가 발송 시 거릅니다.
- 이메일 수신거부.
3) 지연
NCP 080 거부 목록은 1시간마다 동기화됩니다. 또 진행 중인 DB 트랜잭션보다 뒤의 이벤트는 그 트랜잭션이 끝날 때까지 나오지 않습니다(유실이 아니라 대기). 보통 거부 후 1시간 남짓 안에 피드에 나옵니다.
4) 커서 만료(410)와 스냅샷 복구
이벤트는 180일 보존합니다. 저장한 커서가 보존 구간보다 오래되면 410 이 옵니다.
// 410
{ "success": false, "code": "CURSOR_EXPIRED", "message": "since 가 보존 기간을 지났습니다 — ...", "snapshot": "/api/v1/optout/snapshot" }
GET /api/v1/optout/snapshot?afterId={id}&size=1000 — 이 가맹점의 현재 BLOCKED 전체를 id 순으로 줍니다.
- afterId 없이 첫 페이지를 받고 그 응답의 resumeCursor 를 보관합니다(첫 페이지에만 있음, 이후 페이지는 null).
- hasMore 가 false 가 될 때까지 afterId={직전 nextAfterId} 로 이어 받습니다. afterId 가 음수·숫자 아님이면 400 INVALID_AFTER_ID.
- 받은 목록이 현재 거부 전체입니다. 목록에 있는 번호는 거부로 반영하고, 목록에 없는 번호는 연동 쪽에서 080 거부 때문에 막아 둔 것만 풀 수 있습니다(고객이 직접 한 철회 등 다른 경로의 상태는 건드리지 마세요).
- 1)에서 보관한 resumeCursor 를 since 로 피드를 이어 받습니다. 스냅샷을 읽는 동안 생긴 변경은 모두 이 커서 뒤의 이벤트로 다시 오므로 순서대로 적용하면 최신 상태가 됩니다. resumeCursor 는 항상 보존 구간 안이라 곧바로 410 이 다시 나지 않습니다.
새로 연동할 때의 최초 동기화도 같은 절차를 씁니다(피드를 처음부터 읽지 말고 스냅샷 → resumeCursor).
// 스냅샷 첫 페이지
{
"success": true,
"data": {
"list": [ { "id": 101, "receiver": "01000003333", "status": "BLOCKED", "rejectedAt": "2026-08-01T11:00:00", "attribution": "MULTIPLE" } ],
"nextAfterId": 101,
"hasMore": false,
"resumeCursor": "v1.1234567.89"
}
}
5) 409 TENANT_ID_AMBIGUOUS
수신거부 기록의 발송자 칸은 회원·관리자·가맹점 ID 를 한 칸에 담습니다. 그래서 가맹점 ID 가 숫자로만 되어 있거나 관리자 ID 와 같으면 남의 거부가 섞일 수 있어 피드·스냅샷 모두 409 TENANT_ID_AMBIGUOUS 로 막습니다. 해당 가맹점은 ID 변경이 필요하니 운영자에게 문의하세요(2026-09-23 운영 확인 기준 해당 가맹점 0곳).
발신번호 목록
GET /api/v1/sender/list
선택 파라미터 status로 상태 필터가 가능합니다(미지정·ALL 시 전체). 응답에는 count와 함께 동일 배열이 senders·list 두 키로 제공됩니다(연동 호환 별칭). 각 항목은 number(=phone_number)·name 별칭과 원본 필드(phone_number, status, is_default, applicant_name, manager_name, manager_phone 등)를 함께 포함합니다.
// 응답
{
"success": true,
"count": 1,
"senders": [
{
"id": 12,
"number": "0339116510",
"name": "임팩시스 대표",
"phone_number": "0339116510",
"status": "APPROVED",
"is_default": true,
"applicant_name": "임팩시스",
"manager_name": "임팩시스 대표",
"manager_phone": "01012345678",
"applied_at": "2026-05-31T14:00:03+09:00",
"reviewed_at": "2026-05-31T15:20:11+09:00"
}
],
"list": [ ... ]
}
발신번호 상세
GET /api/v1/sender/{id}
발신번호 1건의 상세를 sender 객체로 반환합니다. 존재하지 않으면 404.
// 응답
{
"success": true,
"sender": {
"id": 12,
"phone_number": "0339116510",
"status": "APPROVED",
"is_default": true,
"applicant_name": "임팩시스",
"manager_name": "임팩시스 대표",
"manager_phone": "01012345678",
"applied_at": "2026-05-31T14:00:03+09:00",
"reviewed_at": "2026-05-31T15:20:11+09:00"
}
}
발신번호 등록
POST /api/v1/sender/register (multipart/form-data)
서류 심사형 등록 요청입니다. 등록 직후 상태는 PENDING이며 심사 후 승인됩니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
| phoneNumber | string | 필수 | 등록할 발신번호 |
| applicantName | string | 선택 | 신청자명 |
| managerName | string | 선택 | 담당자명 |
| managerPhone | string | 선택 | 담당자 연락처 |
| certFile | file | 선택 | 통신서비스 이용증명원 |
| bizFile | file | 선택 | 사업자등록증 |
curl -X POST https://imsend.kr/api/v1/sender/register \
-H "X-API-Key: YOUR_API_KEY" \
-F "phoneNumber=0339116510" \
-F "applicantName=임팩시스" \
-F "certFile=@cert.pdf"
# 응답: { "success": true, "phoneNumber": "0339116510", "status": "PENDING", ... }
월별 정산 조회
GET /api/v1/sms/billing?year=YYYY
연도별 발송 집계(월별·분기별)를 반환합니다. year 미지정 시 당해 연도. 잔액·신용한도도 함께 포함됩니다.
GET /api/v1/sms/billing?year=2026
X-API-Key: your_api_key_here
포인트 내역 조회
GET /api/v1/sms/point-history?page=1&size=50
포인트 적립·차감 원장(list)과 충전 결제 이력(payments)을 함께 반환합니다.
// 응답
{
"success": true,
"balance": 4980,
"total": 128,
"list": [
{ "ledger_seq": 1025, "tx_type": "USE", "amount": 12, "balance_after": 4980, "description": "SMS 발송", "reg_date": "2026-05-04T14:25:30" }
],
"payments": [ ... ]
}
amount 는 항상 양수이고 방향은 tx_type 으로 판단합니다 — CHARGE(적립) / USE(선불 사용)·CREDIT_USED(후불 사용)·DEDUCT(관리자 차감)·ADJUSTMENT(결제 취소 등 조정) / REFUND(환불 적립).
메시지 템플릿
SMS/LMS/MMS 본문 템플릿을 등록·관리합니다. 템플릿은 테넌트 단위로 격리됩니다.
목록 조회
GET /api/v1/templates
파라미터: page(기본 1)·size(기본 20)·msgType·keyword(선택). 응답은 data.list 배열과 data.total/page/size.
등록
POST /api/v1/templates
{
"templateName": "예약 안내",
"msgText": "안녕하세요, 예약이 확정되었습니다.",
"msgType": "SMS"
}
// 응답
{ "success": true, "data": { "templateId": "ab12cd34ef56gh78ij90" } }
templateName·msgText는 필수, msgType는 선택입니다.
수정
PUT /api/v1/templates/{templateId}
본문은 등록과 동일(templateName·msgText·msgType). 타 테넌트 소유 템플릿 수정 시 403.
삭제
DELETE /api/v1/templates/{templateId}
웹훅
충전 완료·배치 종료 같은 변화를 폴링 대신 연동 서비스의 URL 로 POST 받습니다. 인증은 다른 가맹점 API 와 같이 X-API-Key(또는 Authorization: Bearer), 없거나 무효면 401. 구독은 가맹점당 1건입니다. 서버에 웹훅 암호화 키가 설정되지 않은 동안은 구독 저장이 503 WEBHOOK_UNAVAILABLE 입니다.
1) 구독 생성·교체
PUT /api/v1/webhook
// 요청
{ "url": "https://example.com/imsend/webhook", "events": ["charge.completed", "batch.finalized"], "include_children": false }
// 응답 — secret 은 이 응답에서만 제공됩니다
{ "success": true, "data": { "url": "https://example.com/imsend/webhook", "events": ["charge.completed", "batch.finalized"], "include_children": false, "secret": "whsec_..." } }
- PUT 은 구독 전체를 바꾸고 시크릿을 매번 새로 발급합니다(이전 시크릿은 즉시 무효 — 아직 전송 대기 중인 이벤트도 새 시크릿으로 서명됩니다). 시크릿을 잃어버렸으면 PUT 을 다시 호출하세요.
- url: https 만, 포트는 기본(443) 또는 443·8443, 사용자정보(user@) 불가. 호스트가 가리키는 주소 중 하나라도 루프백·사설(10/8·172.16/12·192.168/16)·링크로컬·CGNAT(100.64/10)·멀티캐스트·IPv6 ULA 등 내부 주소면 400 INVALID_URL. 이 검사는 등록할 때와 매 전송 직전에 다시 합니다. 리다이렉트(3xx)는 따라가지 않습니다(실패로 처리).
- include_children: true 면 내 하위 가맹점(상위 가맹점이 나인 곳)의 이벤트도 받습니다. 하위 이벤트는 봉투의 tenant_id 가 하위 ID 입니다.
- 지원하지 않는 이벤트가 섞이면 400 INVALID_EVENT.
2) 조회 · 해지
GET /api/v1/webhook — url·events·include_children·status·reg_date·upd_date(시크릿 제외). 없으면 404 NOT_FOUND.
DELETE /api/v1/webhook — 해지. 아직 보내지 않은 이벤트와 이력도 함께 지워집니다. 없으면 404.
3) 이벤트 이력 · 수동 재전송
GET /api/v1/webhook/events?state=&size=20 — 최근 이벤트(최대 100건). state 는 PENDING·DELIVERED·DEAD(틀리면 400 INVALID_STATE), size 는 1 이상(100 초과는 100, 틀리면 400 INVALID_SIZE). 항목: event_id·type·tenant_id·state·attempts·last_status(마지막 HTTP 상태, null=응답 없음·주소 차단)·created_at·delivered_at. 본문은 주지 않습니다.
POST /api/v1/webhook/events/{eventId}/redeliver — 내 구독이 받은 이벤트를 다음 전송 주기(약 15초)에 다시 보냅니다. DEAD 이벤트는 한 번 더 시도하고 실패하면 다시 DEAD 입니다. 다른 가맹점 이벤트·없는 ID 는 404.
4) 이벤트 유형
| type | 발생 시점 | data |
|---|---|---|
| charge.completed | 내 원장에 충전이 적립됨 — 카드 결제(충전 세션·포털), 관리자 적립, 후불 자동충전, POST /api/v1/point/record(CHARGE), 상위 가맹점의 하위 point/record(CHARGE — 자사 상위 계정만 허용, 그 밖은 403 FREE_CHARGE_NOT_ALLOWED) | source(CARD·ADMIN·AUTO_CHARGE·API·ADMIN_API)·order_id(카드만, 그 외 null)·ledger_seq·amount·charge_type·balance_after |
| template.status_changed | 알림톡 템플릿 심사 승인·반려 | template_id·template_code·status·draft_status(재검수 반려는 status 유지, draft_status=REJECTED). 반려 사유는 템플릿 조회 API 로 |
| sender.status_changed | 발신번호 심사 승인·반려 | sender_id·status(APPROVED·REJECTED). 번호는 GET /api/v1/sender/{id} |
| optout.changed | 내 발신의 SMS 수신거부가 차단·해제됨(080 동기화·관리자 처리) | feed="/api/v1/optout" 만 — 번호·상태는 변경분 피드로 읽으세요 |
| wallet.low_balance | 공유 지갑 하위의 발송이 상위 잔액·신용 부족으로 거절됨(주체 = 상위). 같은 구독에 24시간 1회 | consumer_tenant_id·balance·available·required |
| usage_limit.reached | 공유 지갑 하위의 월 사용 한도 초과로 발송이 거절됨(주체 = 하위). 같은 구독에 24시간 1회 | usage_limit_monthly·remaining·required |
| batch.finalized | 배치의 모든 수신 건의 결과가 고정됐을 때 배치당 1회(행마다 최초 종결, 또는 기준 시각(예약 시각, 없으면 접수)+72시간 경과). 마지막 건 접수 뒤 30분은 기다리고, 5분 주기로 확인하므로 수 분 늦게 올 수 있습니다. 구독(재구독) 시점보다 72시간 넘게 앞선 배치는 보내지 않습니다(처음 구독 시 과거 배치 폭주 방지). 결과는 GET /api/v1/sms/batch/{batchId}/results 로 조회하세요 | {batch_id, total} |
이벤트는 해당 업무 변경과 같은 트랜잭션에서 쌓입니다 — 업무가 취소되면 이벤트도 나가지 않습니다. 금액은 원(1 point = 1원) 단위입니다.
5) 요청 형식
POST {구독 URL}
Content-Type: application/json
User-Agent: imsend-webhook/1
X-ImSend-Event-Id: 5b0e... // 이벤트 ID(= 본문 id)
X-ImSend-Event-Type: charge.completed
X-ImSend-Timestamp: 1790000000 // epoch 초
X-ImSend-Signature: v1=3f9a... // 아래 서명
{ "id": "5b0e...", "type": "charge.completed", "created_at": "2026-09-24T10:00:00+09:00", "tenant_id": "venue", "data": { ... } }
- data 에는 ID·상태만 있습니다. 전화번호·메시지 본문·이메일 주소는 싣지 않습니다 — 상세는 해당 조회 API 로 가져오세요.
- 본문의 키 순서·공백은 바뀔 수 있습니다. 서명은 받은 원문 바이트 그대로 검증하세요(파싱 후 다시 직렬화하면 불일치).
- 5초 안에 2xx 로 응답하세요. 응답 본문은 읽지 않습니다. 오래 걸리는 처리는 큐에 넣고 먼저 200 을 주세요.
6) 서명 검증
v1= 뒤는 HMAC-SHA256(key = 시크릿 문자열 전체(whsec_ 포함, UTF-8), message = 타임스탬프 + "." + 원문 본문) 의 소문자 hex 입니다. 타임스탬프가 현재와 ±5분 넘게 차이 나면 거절하고, 비교는 상수 시간 비교를 쓰세요.
// Java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
String expected = "v1=" + HexFormat.of().formatHex(mac.doFinal((ts + "." + rawBody).getBytes(StandardCharsets.UTF_8)));
boolean ok = Math.abs(Instant.now().getEpochSecond() - Long.parseLong(ts)) <= 300
&& MessageDigest.isEqual(expected.getBytes(StandardCharsets.US_ASCII), signature.getBytes(StandardCharsets.US_ASCII));
// Node.js (rawBody 는 Buffer 원문)
const crypto = require('crypto');
const expected = 'v1=' + crypto.createHmac('sha256', secret).update(ts + '.').update(rawBody).digest('hex');
const ok = Math.abs(Date.now() / 1000 - Number(ts)) <= 300
&& expected.length === signature.length
&& crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
7) 재시도 · 중복
- 2xx 가 아니거나(3xx 포함) 연결 실패·5초 초과면 1분 · 5분 · 30분 · 2시간 · 6시간 · 24시간 뒤 다시 보냅니다. 6번 재시도 후에도 실패하면 DEAD 로 멈춥니다(수동 재전송 가능).
- 적어도 한 번 전달입니다. 같은 이벤트가 두 번 이상 올 수 있으니 X-ImSend-Event-Id(= 본문 id)로 중복을 거르세요. 순서도 보장하지 않습니다 — 필요하면 created_at 과 조회 API 로 최신 상태를 확인하세요.
- 전송이 끝난(DELIVERED·DEAD) 이벤트 이력은 30일 보관합니다.
공통 응답 형식
조회 API 는 아래 형식입니다. 예외: POST /api/v1/sms/send 는 data 없이 평면 JSON 으로 응답합니다(SMS 발송 참조).
// 성공
{ "success": true, "data": { ... } }
// 실패 — code 는 호출자가 분기해야 하는 일부 오류에만 옵니다(아래 에러 코드 표)
{ "success": false, "code": "TOO_MANY_RECEIVERS", "message": "clientRequestId 요청은 수신자 1000명 이하" }
// 잔액 부족(402) — code 없음, HTTP 상태로 분기
{ "success": false, "message": "잔액 부족 (잔액: 1,000원, 필요: 1,200원)", "balance": 1000, "available": 1000 }
에러 코드
1) 요청 에러 (HTTP 응답)
요청이 거부되면 HTTP 상태 코드와 함께 { "success": false, "message": "..." } 본문이 반환됩니다. message 에는 한글 사유가 담기며 문구는 바뀔 수 있습니다. 호출자가 분기해야 하는 경우에는 code 필드가 함께 옵니다(아래 표의 코드 — 분기는 code 로 하세요). 하위 가맹점 생성·관리 API 의 에러는 리셀러 API 문서를 참고하세요.
| HTTP | 상황 | message 예시 · 조치 |
|---|---|---|
| 400 | 필수 파라미터 누락·형식 오류 | "to 필수", "유효하지 않은 전화번호", "올바른 email 형식이 아닙니다" → 요청 형식 확인 |
| 401 | API Key 인증 실패 | "유효하지 않은 API Key" → X-API-Key 헤더 확인·재발급 |
| 402 | 포인트(잔액) 부족 · 후불 신용한도 초과 (code 없음) | "잔액 부족 (잔액: …원, 필요: …원)" / 후불 "신용한도 초과 (가용: …원, 필요: …원)". 문자·예약·알림톡·이메일 단건 발송은 balance·available 필드를 함께 싣습니다(이메일 대량 /email/send/bulk 는 없음) → 충전 후 재시도 |
| 404 | 존재하지 않는 발신번호 | "존재하지 않는 발신번호입니다." → 발신번호 등록·확인 |
| 409 | 발신번호 중복 등록 · 취소할 수 없는 예약 | "이미 등록된 발신번호입니다: …" → 중복 요청 제거 / "취소할 수 없는 상태입니다 (현재: SENDING)" → 이미 발송 처리가 시작된 예약(취소 불가) |
| 400 | code 있는 입력 오류 | INVALID_CLIENT_REQUEST_ID·TOO_MANY_RECEIVERS(발송, 중복 방지 키) / INVALID_CURSOR·INVALID_SIZE·INVALID_AFTER_ID(수신거부 변경분) |
| 409 | TENANT_ID_AMBIGUOUS | 가맹점 ID 가 회원·관리자 ID 와 구분되지 않아 수신거부 변경분을 줄 수 없음 → 운영자 문의(ID 변경) |
| 410 | CURSOR_EXPIRED | 수신거부 피드 커서가 보존 기간(180일)을 지남 → 스냅샷으로 다시 맞춘 뒤 resumeCursor 부터 이어받기 |
| 503 | IN_PROGRESS | 같은 clientRequestId 요청을 처리 중(Retry-After: 30) → 30초 뒤 같은 키로 재호출. 미접수로 판단하지 마세요 |
| 503 | 이메일 서비스 비활성화 | "이메일 발송 서비스가 비활성화 상태입니다" → 관리자 문의 |
| 500 | 서버 내부 오류 · 발송 대기열 적재 실패 | "Internal server error", "발송 실패: …" → 잠시 후 재시도·문의. 알림톡 발송 중 대기열 적재 오류가 나면 { "success": false, "code": "QUEUE_FAIL" } — 이력은 FAILED/QUEUE_FAIL 로 남고 과금되지 않습니다. 드물게 연결 단절로 실제 발송됐을 수 있으니 발송 이력을 확인한 뒤 재시도하세요 |
2) 발송 상태 (status)
발송 이력(/api/v1/sms/logs) 응답의 status 값. 비동기 발송이라 발송 직후엔 QUEUED→PROCESSING→…으로 진행합니다.
| status | 의미 |
|---|---|
| QUEUED | 발송 큐 적재(비동기 대기) |
| PROCESSING | GT Agent 전달·발송 중 |
| SENT / PENDING | 발송 요청 완료, 통신사 결과 대기 |
| DELIVERED | 수신 완료 — 이 시점에 과금 확정 |
| RESERVED | 예약 발송 대기 — 발송 시각에 잔액(후불은 신용한도 포함)을 다시 확인하고, 부족하면 FAILED(INSUFFICIENT_BALANCE)로 끝납니다(과금 없음) |
| SENDING | 예약 발송 처리 중 — 이 상태부터는 취소할 수 없습니다(취소 요청은 409) |
| FAILED | 발송 실패(사유는 failReason 참조) |
| TIMEOUT | 통신사 결과 미수신 — 자동 환불 대상 |
| CANCELLED | 예약 취소됨 |
3) 발송 결과 코드 (failReason)
status가 FAILED/TIMEOUT일 때 failReason(내부 result_code)에 담기는 값. 성공 시엔 통신사 코드 0 또는 SUCCESS.
| failReason | 의미 · 조치 |
|---|---|
| 0 / SUCCESS | 정상 발송·수신 |
| QUEUE_FAIL / QUEUE_FAILED | 발송 큐 적재 실패 → 재발송 |
| QUEUED_TIMEOUT | 큐 적재 후 10분 내 미처리 → 재발송 |
| OUTBOX_TIMEOUT | 발송 이벤트 처리 지연 → 재발송 |
| NO_MSEQ | 발송 번호(MSEQ) 미확인 — 문자는 GT 미전달, 알림톡은 실제 발송됐을 수 있음 → 수신 여부 확인 후 재발송 |
| NO_RESULT | 통신사 결과 미수신(TIMEOUT) → 자동 환불 |
| TIMEOUT_REFUNDED | TIMEOUT 건 포인트 자동 환불 완료 |
| RESERVE_ERR | 예약 발송 처리 오류 → 재예약 |
| INSUFFICIENT_BALANCE | 예약 발송 시점 잔액 부족 — 과금 없음 → 충전 후 재예약 |
| 102 | GT Agent 인증 실패(일시적) → 잠시 후 재발송 |
| 913 | 알림톡 카카오 인증 실패 → 발신프로필·템플릿(SENDER_KEY/TEMPLATE_CODE) 확인 |
※ 0 이외의 숫자 코드는 통신사/게이트웨이가 반환한 원본 결과 코드입니다. 위에 없는 숫자가 보이면 발신번호·수신번호 상태(결번·수신거부 등)를 확인하거나 문의해 주세요.