문자 메시지

View as Markdown

ClawOps는 하나의 엔드포인트로 SMS·LMS·MMS를 모두 발송합니다. 본문 길이와 첨부 유무에 맞는 Type만 지정하면 통신사까지의 전달은 플랫폼이 처리합니다.

POST /v1/accounts/{accountId}/messages

성공하면 201과 함께 접수된 메시지가 돌아옵니다. 통신사 리포트는 그보다 늦게 도착하므로 최종 발송 결과는 webhook으로 받습니다.

지원 채널은 SMS·LMS·MMS 세 가지입니다. iMessage, RCS와 카카오 알림톡은 발송할 수 없습니다.

채널

SMSLMSMMS
본문 한도200 byte2,000자2,000자
제목 (Subject)불가가능가능
이미지 첨부불가불가최대 3장

한도의 기준이 서로 다릅니다. SMS는 UTF-8 바이트로, LMS와 MMS는 글자 수로 셉니다. 한글은 한 자에 3바이트이므로 한글로만 작성하면 SMS에 66자까지 들어가고, 영문과 숫자를 섞으면 더 많이 들어갑니다.

본문이 200byte를 넘길지 확실하지 않다면 처음부터 Typelms로 지정하세요. sms로 보냈다가 한도를 넘기면 발송되지 않고 400이 반환됩니다. 본문에 이름 같은 변수를 넣는 경우 이름이 긴 수신자에서만 실패하므로 뒤늦게 발견됩니다.

메시지 보내기

필드타입설명
Tostring필수. 수신번호입니다. 국내 표기와 +82 E.164를 모두 받아 정규화합니다.
Fromstring필수. 발신번호입니다. 계정에 등록된 번호만 사용할 수 있습니다.
Bodystring필수. 본문입니다. SMS는 200byte, LMS와 MMS는 2,000자까지 허용합니다.
Typestringsms, lms, mms 중 하나입니다. 기본값은 sms입니다.
SubjectstringLMS와 MMS의 제목입니다. SMS에는 사용할 수 없습니다.
MediaUrlstring[]MMS 첨부 이미지 URL입니다. 최대 3개까지 허용합니다.
$curl -X POST "https://api.claw-ops.com/v1/accounts/YOUR_ACCOUNT_ID/messages" \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "To": "01012345678",
> "From": "07012341234",
> "Body": "주문하신 상품이 오늘 출고되었습니다."
> }'
응답 (201)
1{
2 "messageId": "MG3f7a1c9e2b8d4f0a6c5e1b9d7a3f2c48",
3 "status": "queued",
4 "type": "sms",
5 "to": "01012345678",
6 "from": "07012341234",
7 "body": "주문하신 상품이 오늘 출고되었습니다.",
8 "subject": null,
9 "numMedia": 0,
10 "mediaUrl": [],
11 "accountId": "YOUR_ACCOUNT_ID",
12 "direction": "outbound",
13 "dateCreated": "2026-08-10T04:12:44.123Z"
14}

Python은 예약어를 피해 from_을 사용합니다. 두 SDK 모두 메서드 이름은 create입니다.

statusqueued는 발송 요청이 접수됐다는 뜻이며 전달 성공을 보장하지 않습니다. 이 시점에는 통신사 리포트가 돌아오기 전이라 성공과 실패를 알 수 없습니다. 최종 결과는 webhook으로 확인하세요.

수신번호

To는 국내 표기와 +82 E.164를 모두 받습니다. 공백, 하이픈, 괄호, 점은 제거되므로 010-1234-567801012345678은 같은 값입니다. 저장과 조회는 정규화된 형태로 이뤄집니다.

입력결과
010-1234-5678 · +82 10-1234-567801012345678
02-123-4567 · 070-1234-5678 · 0507-1234-5678그대로(구분자만 제거)
1588-123415881234 (대표번호 8자리)
+1 415 555 0100400 — 국제 발신은 지원하지 않습니다
1012345678 (앞 0 없음)400 — 형식 오류

발신번호

From은 필수이며 계정이 보유한 번호여야 합니다. 보유하지 않은 번호를 지정하면 400이 반환됩니다. 발신번호를 자동으로 고르는 동작은 없습니다. 수신번호와 달리 From은 하이픈만 제거하고 숫자 3~12자리인지만 확인합니다.

이미지 첨부

Typemms로 지정하고 MediaUrl에 이미지 URL을 전달합니다.

$curl -X POST "https://api.claw-ops.com/v1/accounts/YOUR_ACCOUNT_ID/messages" \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "To": "01012345678",
> "From": "07012341234",
> "Type": "mms",
> "Subject": "촬영본 안내",
> "Body": "요청하신 사진 보내 드립니다.",
> "MediaUrl": [
> "https://example.com/room-1.jpg",
> "https://example.com/room-2.jpg"
> ]
> }'
항목제한
첨부 장수최대 3장
형식jpg, jpeg, png, bmp
장당 크기300KB (307,200 byte). 변환 전 내려받은 원본 기준입니다.
다운로드 제한시간10초

URL은 인증 없이 접근할 수 있는 주소여야 합니다. 플랫폼이 직접 내려받아 통신사로 전달하므로 인증이 필요한 주소나 내부망 주소는 거절됩니다.

형식은 두 단계로 검사합니다. URL 경로의 확장자가 위 목록에 있어야 하고, 내려받은 응답의 Content-Typeimage/jpeg, image/png, image/bmp 중 하나여야 합니다. 확장자가 드러나지 않는 URL은 내용과 무관하게 거절되므로 .jpg처럼 확장자가 붙은 주소를 사용하세요.

PNG와 BMP는 서버가 JPG로 변환해 발송합니다. 통신사가 MMS에서 PNG와 BMP를 거부하기 때문입니다. 변환 과정에서 투명 배경은 흰색으로 합성되고 EXIF 회전 정보는 픽셀에 반영됩니다. 원본이 이미 JPEG이면 재인코딩하지 않고 그대로 씁니다.

첨부 없이 Typemms로 지정하면 전달 자체는 LMS와 같아지지만, 한도와 과금은 MMS로 잡힙니다. 첨부가 없다면 처음부터 lms를 사용하세요.

조회

발송과 수신 이력을 한 목록에서 조회합니다. 응답의 direction으로 구분하며 정렬은 최신순 고정입니다.

GET /v1/accounts/{accountId}/messages
쿼리타입설명
typestringsms, lms, mms로 필터합니다.
statusstringqueued, sent, failed, received로 필터합니다.
numberstring발신·수신 양쪽에서 매칭합니다. 하이픈 유무를 모두 흡수합니다.
pageinteger페이지 번호입니다. 기본값은 0이며 0부터 시작합니다.
pageSizeinteger페이지당 건수입니다. 기본값은 20입니다. 100을 넘기면 400입니다.
$curl "https://api.claw-ops.com/v1/accounts/YOUR_ACCOUNT_ID/messages?type=sms&pageSize=20" \
> -H "Authorization: Bearer YOUR_API_KEY"
응답
1{
2 "data": [
3 {
4 "messageId": "MG3f7a1c9e2b8d4f0a6c5e1b9d7a3f2c48",
5 "accountId": "YOUR_ACCOUNT_ID",
6 "direction": "outbound",
7 "type": "sms",
8 "from": "07012341234",
9 "to": "01012345678",
10 "subject": null,
11 "body": "주문하신 상품이 오늘 출고되었습니다.",
12 "status": "sent",
13 "numMedia": 0,
14 "mediaUrl": [],
15 "dateCreated": "2026-08-10T04:12:44.123Z",
16 "dateUpdated": "2026-08-10T04:12:51.884Z"
17 }
18 ],
19 "meta": { "page": 0, "pageSize": 20, "total": 1 }
20}

단건 조회와 첨부 원본 조회는 다음 두 엔드포인트를 사용합니다.

GET /v1/accounts/{accountId}/messages/{messageId}
GET /v1/accounts/{accountId}/messages/{messageId}/media/{index}

index는 0부터 시작합니다. 목록·단건 응답의 mediaUrl과 수신 webhook의 MediaUrl{i}는 모두 이 엔드포인트를 가리키는 절대 URL입니다 — 저장소 주소가 아닙니다.

1"mediaUrl": [
2 "https://api.claw-ops.com/v1/accounts/AC1a2b3c4d/messages/MG3f7a…/media/0",
3 "https://api.claw-ops.com/v1/accounts/AC1a2b3c4d/messages/MG3f7a…/media/1"
4]

이 URL도 API 키 인증이 필요합니다. 첨부는 전부 JPEG으로 변환된 뒤 저장되므로 image/jpeg로 내려옵니다.

대화 스레드 API는 제공하지 않습니다. 상대 번호별로 주고받은 이력을 묶으려면 number로 필터하거나 목록 응답을 fromto 기준으로 직접 그룹핑하세요.

발송 결과와 수신 문자

발송 결과와 수신 문자는 모두 계정에 등록한 webhook으로 전달됩니다. 메시지마다 콜백 URL을 지정하는 파라미터는 없습니다.

이벤트발생 시점
message.sent통신사로부터 발송 성공 리포트를 받은 직후입니다.
message.failed발송이 실패로 종료된 직후입니다.
message.received보유 번호로 문자가 도착했을 때입니다.
$curl -X POST "https://api.claw-ops.com/v1/accounts/YOUR_ACCOUNT_ID/webhooks" \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://example.com/message-webhook",
> "events": ["message.sent", "message.failed", "message.received"]
> }'

상태는 queued에서 출발해 sent 또는 failed로 끝납니다. 그 사이의 중간 상태는 없습니다. 수신 메시지는 received로 기록됩니다.

페이로드

모든 요청은 Content-Type: application/x-www-form-urlencoded POST입니다.

파라미터이벤트설명
MessageId공통메시지 고유 ID입니다.
AccountId공통계정 ID입니다.
From, To공통발신번호와 수신번호입니다.
Direction공통발송은 outbound, 수신은 inbound입니다.
Type공통sms, lms, mms 중 하나입니다.
Status공통sent, failed, received 중 하나입니다.
Timestamp공통이벤트 발생 시각입니다. ISO 8601 형식입니다.
Subject, Bodymessage.received수신 메시지의 제목과 본문입니다. 없으면 빈 문자열입니다.
NumMediamessage.received첨부 개수입니다. 없으면 0입니다.
MediaUrl0, MediaUrl1message.received첨부 저장 경로입니다. NumMedia 만큼 전달됩니다.

발송 결과 webhook에는 Subject·Body·NumMedia가 실리지 않습니다. 본문이 필요하면 MessageId로 단건 조회하세요.

1from flask import Flask, request
2
3app = Flask(__name__)
4
5@app.route("/message-webhook", methods=["POST"])
6def message_webhook():
7 status = request.form.get("Status")
8 message_id = request.form.get("MessageId")
9
10 if status == "received":
11 print(f"수신 [{message_id}]: {request.form.get('Body', '')}")
12 elif status == "failed":
13 print(f"발송 실패 [{message_id}] → {request.form.get('To')}")
14
15 return "", 204

MediaUrl{i}는 첨부 조회 엔드포인트를 가리키는 절대 URL입니다. 목록·단건 조회 응답의 mediaUrl과 같은 주소이며, 그대로 요청하면 원본이 내려옵니다.

이 URL도 API 키 인증이 필요합니다. Authorization 헤더 없이 요청하면 401입니다. 공개 저장소 주소가 아닙니다.

요청에는 X-Signature 헤더가 포함됩니다. 계정의 Signing Key로 HMAC-SHA256 서명을 검증한 뒤 처리하세요.

실패 사유는 전달되지 않습니다. message.failed는 실패했다는 사실만 알려주며 통신사 결과코드는 포함되지 않습니다.

제한과 함정

발송이 거절되는 경우

에러 응답의 형태는 어디서 막혔는지에 따라 두 가지입니다.

요청 형식 검증에서 막히면 errors 배열과 code가 함께 옵니다. 어느 필드가 왜 틀렸는지는 errors[].path를 보세요.

형식 검증 실패
1{
2 "error": "request/body/MediaUrl must NOT have more than 3 items",
3 "errors": [
4 {
5 "path": "/body/MediaUrl",
6 "message": "must NOT have more than 3 items",
7 "errorCode": "maxItems.openapi.validation"
8 }
9 ],
10 "code": "VALIDATION"
11}

형식을 통과한 뒤 업무 규칙에서 막히면 errors 배열 없이 errorcode만 옵니다.

업무 규칙 위반
1{ "error": "SMS Body는 200byte를 초과할 수 없습니다", "code": "body_too_long" }

분기는 code로 하세요. error 문구는 안내를 다듬으면서 바뀔 수 있지만 code는 계약입니다. 같은 422라도 recipient_blocked(명단에서 해제해야 나감)와 quota_exceeded(다음 주기에 다시 나감)는 대응이 정반대입니다.

후자에 해당하는 경우는 다음과 같습니다.

상태code메시지원인
400invalid_inputTo, From, Body는 필수입니다필드는 있으나 빈 문자열입니다.
400invalid_phone전화번호 형식이 올바르지 않습니다To가 정규화 규칙에 맞지 않습니다. 앞 0이 빠진 번호가 대표적입니다.
400invalid_phone국제 발신은 지원하지 않습니다To+82 외의 국가번호입니다.
400invalid_phoneFrom은 유효한 전화번호 형식이어야 합니다하이픈 제거 후 숫자 3~12자리가 아닙니다.
400from_not_registeredFrom 번호가 계정에 등록되지 않았습니다보유하지 않은 발신번호입니다.
400body_too_longSMS Body는 200byte를 초과할 수 없습니다SMS 본문 한도를 넘었습니다. lms로 보내세요.
400body_too_longLMS Body는 2000자를 초과할 수 없습니다본문 한도를 넘었습니다. mms로 보낸 경우 MMS Body는…로 반환됩니다.
400sms_no_mediaSMS에는 MediaUrl을 첨부할 수 없습니다첨부는 MMS에서만 가능합니다.
400sms_no_subjectSMS에는 Subject를 설정할 수 없습니다제목은 LMS와 MMS에서만 가능합니다.
400lms_no_mediaLMS에는 MediaUrl을 첨부할 수 없습니다. MMS를 사용하세요첨부가 있으면 mms로 보내세요.
400invalid_media_ext지원하지 않는 이미지 확장자입니다: gifjpg, jpeg, png, bmp 외의 확장자입니다. 확장자가 없으면 (없음)이 붙습니다.
400media_download_failed이미지 처리 실패 (0): SIZE_EXCEEDED300KB를 넘었습니다. 괄호 안 숫자는 몇 번째 첨부인지를 나타냅니다.
400media_download_failed이미지 처리 실패 (0): HTTP 404URL에 접근할 수 없습니다. 상태코드가 그대로 붙습니다.
400media_download_failed이미지 처리 실패 (0): CONTENT_TYPE:text/html확장자는 맞지만 응답의 Content-Type이 이미지가 아닙니다.
403no_active_subscription활성 구독이 없습니다구독이 없거나 만료됐습니다.
403messaging_blocked문자 발송이 제한된 계정입니다. 고객센터로 문의해 주세요.운영상 발송이 차단된 계정입니다.
422recipient_blocked수신거부 명단에 등록된 번호입니다. 해제 후 다시 시도하세요message 채널 수신거부 명단에 있는 번호입니다.
422type_not_supportedSMS가 지원되지 않는 플랜입니다.해당 타입의 한도가 0인 플랜입니다. 타입에 따라 LMS, MMS로 바뀝니다.
422quota_exceededSMS quota exceeded (120/100)월 한도를 초과했고 초과분 부가서비스가 없습니다.
422override_quota_exceeded문자 발송 한도를 초과했습니다 (120/100)운영상 걸린 계정별 총 발송 상한을 넘었습니다. 타입과 무관한 합계 기준입니다.

첨부 처리 실패는 모두 이미지 처리 실패 (i): <원인> 형태로 옵니다. 위 세 가지 외에 다운로드가 10초를 넘긴 경우도 같은 형태로 반환됩니다. 괄호 안 숫자는 MediaUrl 배열에서 몇 번째 첨부인지를 가리키므로, 여러 장을 보낼 때 어느 URL이 문제인지 이걸로 찾으세요.

월 한도

플랜마다 SMS, LMS, MMS 각각의 월 발송 건수 한도가 있습니다. 한도를 넘겨도 해당 타입의 초과분 부가서비스가 활성이면 발송이 계속되고 초과분이 과금됩니다. 부가서비스가 없으면 422로 거절됩니다.

집계 대상은 발신 중 실패하지 않은 건입니다. queued 상태도 포함되며 failed로 끝난 건은 빠집니다.

한도는 매월 초기화됩니다. 결제 주기가 아니라 월 단위입니다. 여러 달을 선납한 계정도 한도는 매달 리셋됩니다.

수신거부 명단

수신거부 명단에 등록된 번호로는 문자가 나가지 않습니다. 등록 즉시 발신 직전에 막히며 422 recipient_blocked로 거절됩니다. 배치 발신(캠페인)도 같은 관문을 지나므로 해당 대상은 자동으로 건너뜁니다.

등록
$curl -X POST "https://api.claw-ops.com/v1/accounts/YOUR_ACCOUNT_ID/blocked-recipients" \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "number": "01012345678", "channel": "message" }'

해제는 DELETE /v1/accounts/{accountId}/blocked-recipients/{blockId}입니다.

차단된 발송은 메시지 이력에 남지 않습니다. 발송이 일어나지 않았으므로 기록할 것이 없기 때문입니다. 목록 조회에서 찾지 못했다고 다시 보내지 마세요 — 명단에서 해제해야 나갑니다.

등록은 멱등입니다. 이미 차단 중인 (번호, 채널) 조합을 다시 등록하면 에러 없이 기존 항목을 200으로 돌려줍니다. 새로 등록된 경우에만 201입니다.

대상은 이 계정의 발신입니다. 그 번호에서 걸려오거나 도착하는 것은 막지 않습니다.

채널 값은 callmessage입니다. sms가 아닙니다. 전화와 문자를 모두 차단하려면 두 채널로 각각 등록해야 합니다.

광고성 문자

광고성 문자에는 (광고) 표기와 무료 수신거부 방법 안내 등 법적 의무가 따릅니다. 야간 발송 제한(21시~익일 8시)도 적용됩니다.

야간 발송 제한은 플랫폼이 막아주지 않습니다. 배치 발신(전화)에는 시간대 게이트가 걸려 있지만 문자 발송 경로에는 없습니다. 밤에 요청하면 밤에 나갑니다. 발송 시각은 직접 통제하세요.