📱 imsend REST API v1

SMS / LMS / MMS / 이메일 API

HTTP POST 요청으로 문자·이메일을 발송합니다. 발신번호는 사전에 등록된 번호만 사용 가능합니다.

Base URL
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로 전환됩니다.

요청 파라미터

필드 타입 필수 설명
receiverstring필수수신번호 (하이픈 제거, 예: 01012345678)
messagestring필수발송 내용 (최대 90자)
callbackstring선택발신번호(sender 별칭). 승인(APPROVED)된 번호만 그대로 씁니다. 미입력이면 승인 기본번호(없으면 시스템 대표번호), 미승인 번호를 지정하면 거부하지 않고 같은 규칙으로 대체한 뒤 응답 senderWarning(대체된 번호가 요청 번호와 같으면 — 예: 시스템 대표번호 지정 — 싣지 않음) 으로 알립니다. 실제 사용 번호는 응답 sender
clientRequestIdstring선택중복 발송 방지 키. ^[A-Za-z0-9:_.-]{1,100}$, 가맹점 안에서 요청마다 유일해야 합니다. 보내면 수신자 1,000명 이하·전부 아니면 0 적재·같은 키 재호출 시 원 응답 반환이 적용됩니다 — 중복 방지 키 참조 (2026-09-23 추가, 보내지 않으면 기존 동작 그대로)
adFormatstring선택"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
}
⚠️ 발송은 비동기로 처리됩니다. 수신자별 결과는 GET /api/v1/sms/batch/{batchId}/results(배치 단위, 72시간 뒤 상태 고정·과금액 포함)나 발송 이력(/api/v1/sms/logs)으로 확인합니다.
/api/v1/sms/send 응답은 data 로 감싸지 않은 평면 JSON 입니다. 다른 조회 API({success, data})와 형식이 다르니 파서를 따로 두세요. 이 형식은 하위 호환 때문에 유지합니다.

중복 방지 키 (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 · bytesSMS/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

파라미터 타입 설명
sizeinteger페이지당 건수 (기본: 20)
pageinteger페이지 번호 (기본: 1)
startDatestring시작일 (YYYY-MM-DD)
endDatestring종료일 (YYYY-MM-DD)
msgTypestringSMS / LMS / MMS / EMAIL (미지정·ALL 시 전체)
statusstring상태 필터 (예: 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수신번호 원문(숫자). 받은 쪽에서 로그·화면에 그대로 남기지 마세요
statusBLOCKED = 수신 거부. RELEASED = imsend 운영자가 그 거부를 해제함(오등록 정정·본인 철회 확인 등) — 거부 때문에 연동 쪽에서 막아 둔 것을 풀어도 된다는 뜻이지, 수신 동의를 새로 받았다는 뜻은 아닙니다
rejectedAt / releasedAt거부 시각 / 해제 시각(KST, 해제 전이면 null)
attributionSINGLE | 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 순으로 줍니다.

  1. afterId 없이 첫 페이지를 받고 그 응답의 resumeCursor 를 보관합니다(첫 페이지에만 있음, 이후 페이지는 null).
  2. hasMore 가 false 가 될 때까지 afterId={직전 nextAfterId} 로 이어 받습니다. afterId 가 음수·숫자 아님이면 400 INVALID_AFTER_ID.
  3. 받은 목록이 현재 거부 전체입니다. 목록에 있는 번호는 거부로 반영하고, 목록에 없는 번호는 연동 쪽에서 080 거부 때문에 막아 둔 것만 풀 수 있습니다(고객이 직접 한 철회 등 다른 경로의 상태는 건드리지 마세요).
  4. 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이며 심사 후 승인됩니다.

필드 타입 필수 설명
phoneNumberstring필수등록할 발신번호
applicantNamestring선택신청자명
managerNamestring선택담당자명
managerPhonestring선택담당자 연락처
certFilefile선택통신서비스 이용증명원
bizFilefile선택사업자등록증
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 형식이 아닙니다" → 요청 형식 확인
401API Key 인증 실패"유효하지 않은 API Key" → X-API-Key 헤더 확인·재발급
402포인트(잔액) 부족 · 후불 신용한도 초과 (code 없음)"잔액 부족 (잔액: …원, 필요: …원)" / 후불 "신용한도 초과 (가용: …원, 필요: …원)". 문자·예약·알림톡·이메일 단건 발송은 balance·available 필드를 함께 싣습니다(이메일 대량 /email/send/bulk 는 없음) → 충전 후 재시도
404존재하지 않는 발신번호"존재하지 않는 발신번호입니다." → 발신번호 등록·확인
409발신번호 중복 등록 · 취소할 수 없는 예약"이미 등록된 발신번호입니다: …" → 중복 요청 제거 / "취소할 수 없는 상태입니다 (현재: SENDING)" → 이미 발송 처리가 시작된 예약(취소 불가)
400code 있는 입력 오류INVALID_CLIENT_REQUEST_ID·TOO_MANY_RECEIVERS(발송, 중복 방지 키) / INVALID_CURSOR·INVALID_SIZE·INVALID_AFTER_ID(수신거부 변경분)
409TENANT_ID_AMBIGUOUS가맹점 ID 가 회원·관리자 ID 와 구분되지 않아 수신거부 변경분을 줄 수 없음 → 운영자 문의(ID 변경)
410CURSOR_EXPIRED수신거부 피드 커서가 보존 기간(180일)을 지남 → 스냅샷으로 다시 맞춘 뒤 resumeCursor 부터 이어받기
503IN_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발송 큐 적재(비동기 대기)
PROCESSINGGT 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_REFUNDEDTIMEOUT 건 포인트 자동 환불 완료
RESERVE_ERR예약 발송 처리 오류 → 재예약
INSUFFICIENT_BALANCE예약 발송 시점 잔액 부족 — 과금 없음 → 충전 후 재예약
102GT Agent 인증 실패(일시적) → 잠시 후 재발송
913알림톡 카카오 인증 실패 → 발신프로필·템플릿(SENDER_KEY/TEMPLATE_CODE) 확인

※ 0 이외의 숫자 코드는 통신사/게이트웨이가 반환한 원본 결과 코드입니다. 위에 없는 숫자가 보이면 발신번호·수신번호 상태(결번·수신거부 등)를 확인하거나 문의해 주세요.

(주)임팩시스 · imsend REST API v1 info@imcorp.kr