대량 문자 API v1
소프트웨어에서 대량 문자를 보내기 위한 작은 REST API예요. JSON으로 입력과 출력을 하고, Bearer 키를 사용하며, 콘솔과 동일한 가격과 확인 기능, 그리고 전송 리포트를 위한 서명된 웹훅을 제공해요. 기본 URL https://smsbulk.cc/rest/v1
개요
한 요청당 최대 8,000개의 번호로 보낼 수 있어요. 각 번호는 자신의 병합 값을 가지고 있으며, API는 각 수신자에게 병합된 텍스트를 기준으로 정확히 콘솔처럼 가격을 책정해요. 비용을 알기 위해 동일한 본문으로 먼저 견적을 요청하세요. 견적은 전송되거나 청구되지 않아요. 시간은 ISO 8601 형식의 UTC로, 금액은 소수점 다섯 자리의 미국 달러 문자열로 표시돼요.
curl https://smsbulk.cc/rest/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "to": "+14155550142", "fields": { "name": "Maria" } },
{ "to": "+447400123456", "fields": { "name": "Tom" } }
],
"text": "Hi {{name}}, your order ships today.",
"client_ref": "order-batch-17"
}'const res = await fetch("https://smsbulk.cc/rest/v1/send", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
recipients: [
{ to: "+14155550142", fields: { name: "Maria" } },
{ to: "+447400123456", fields: { name: "Tom" } },
],
text: "Hi {{name}}, your order ships today.",
client_ref: "order-batch-17",
}),
});
const data = await res.json();
console.log(res.status, data.campaign?.id, data.campaign?.cost);import requests
res = requests.post(
"https://smsbulk.cc/rest/v1/send",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"recipients": [
{"to": "+14155550142", "fields": {"name": "Maria"}},
{"to": "+447400123456", "fields": {"name": "Tom"}},
],
"text": "Hi {{name}}, your order ships today.",
"client_ref": "order-batch-17",
},
timeout=30,
)
data = res.json()
print(res.status_code, data.get("campaign", {}).get("id"))<?php
$body = json_encode([
"recipients" => [
["to" => "+14155550142", "fields" => ["name" => "Maria"]],
["to" => "+447400123456", "fields" => ["name" => "Tom"]],
],
"text" => "Hi {{name}}, your order ships today.",
"client_ref" => "order-batch-17",
]);
$ch = curl_init("https://smsbulk.cc/rest/v1/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$data = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $data["campaign"]["id"] ?? "", PHP_EOL;인증
콘솔의 API 및 웹훅 섹션에서 키를 생성하세요. 첫 번째 크레딧 충전 후 키가 활성화돼요. 모든 요청에 Bearer 토큰으로 키를 보내세요. 키는 한 번만 표시되며, 최대 5개의 키를 유지하고 언제든지 취소할 수 있어요.
Authorization: Bearer smsb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx요금
공개적으로, 키가 필요 없어요. 모든 목적지 또는 하나의 ISO 코드에 대한 SMS 세그먼트당 가격을 제공해요.
GEThttps://smsbulk.cc/rest/v1/rates
GEThttps://smsbulk.cc/rest/v1/rates/{ISO}
{
"country": "US",
"name": "United States",
"dial_code": "+1",
"rate": "0.00485",
"currency": "USD",
"per": "message segment"
}크레딧
GEThttps://smsbulk.cc/rest/v1/credit
{ "credit": "125.40000", "currency": "USD", "account": "A418820037715" }견적
전송하지 않고 비용을 계산해요. Send와 동일한 본문을 사용해요.
POSThttps://smsbulk.cc/rest/v1/quote
| 필드 | 타입 | 의미 |
|---|---|---|
| to | string or string[] | 한 개의 번호 또는 목록이에요. +가 포함된 국제 형식이 가장 안전해요. 지역 번호는 default_country가 필요해요. |
| recipients | object[] | to 대신 { to, fields } 목록을 사용해요. 여기서 fields는 {{name}}과 같은 병합 필드를 채워요. |
| text | string | 메시지는 병합 후 최대 2,000자 및 10세그먼트까지 가능해요. |
| default_country | string | + 없이 작성된 번호에 사용되는 ISO 코드예요. 예: GB. 설정에 저장된 기본값으로 돌아가요. |
| opt_out | boolean | 저장된 수신 거부 문구를 마지막 줄에 추가해요 (먼저 설정에 저장하세요). |
{
"recipients": 2,
"messages": 2,
"segments": { "min": 1, "max": 1 },
"encoding": "GSM-7",
"total": "0.01240",
"currency": "USD",
"credit": "125.40000",
"credit_after": "125.38760",
"countries": [
{ "country": "US", "recipients": 1, "rate": "0.00485", "segments": 1, "subtotal": "0.00485" },
{ "country": "GB", "recipients": 1, "rate": "0.00755", "segments": 1, "subtotal": "0.00755" }
],
"left_out": []
}보내기
캠페인을 생성하고, 크레딧에서 총액을 차감한 후 메시지를 큐에 넣어요. 캠페인과 함께 201 응답을 반환해요. 전송할 수 없는 번호는 left_out에 나열되며 청구되지 않아요.
POSThttps://smsbulk.cc/rest/v1/send
| 필드 | 타입 | 의미 |
|---|---|---|
| to / recipients / text / default_country / opt_out | Quote와 같아요. | |
| send_at | string | 시작할 ISO 8601 시간이에요. 5분에서 90일 앞당길 수 있어요. 생략하면 지금 보내요. |
| name | string | 캠페인의 레이블이에요. 최대 80자까지 가능해요. |
| client_ref | string | 자신의 참조값이에요. 최대 64자까지 가능하며 모든 메시지와 웹훅에 반환돼요. |
HTTP/1.1 201 Created
{
"campaign": {
"id": "C507318264409",
"name": null,
"status": "sending",
"source": "api",
"test": false,
"recipients": 2,
"cost": "0.01240",
"refunded": "0.00000",
"net_cost": "0.01240",
"currency": "USD",
"send_at": null,
"created_at": "2026-11-07T14:02:11.000Z",
"started_at": "2026-11-07T14:02:11.000Z",
"finished_at": null,
"client_ref": "order-batch-17"
},
"messages": 2,
"left_out": []
}캠페인
GEThttps://smsbulk.cc/rest/v1/campaigns?limit=25&before={id}
GEThttps://smsbulk.cc/rest/v1/campaigns/{id}
GEThttps://smsbulk.cc/rest/v1/campaigns/{id}/messages?status=failed&limit=100&after={message id}
단일 캠페인에는 큐 대기, 전송, 전달, 실패, 취소 상태별 카운트가 포함돼요. after와 has_more가 있는 메시지 목록 페이지는 페이지당 최대 1,000개까지 표시돼요.
메시지
GEThttps://smsbulk.cc/rest/v1/messages/{id}
GEThttps://smsbulk.cc/rest/v1/messages?to=%2B14155550142&status=delivered&client_ref=order-batch-17&limit=50
{
"id": "M220947781305",
"campaign": "C507318264409",
"to": "+14155550142",
"country": "US",
"segments": 1,
"encoding": "GSM-7",
"price": "0.00485",
"status": "delivered",
"reason": null,
"reason_text": null,
"refunded": false,
"client_ref": "order-batch-17",
"created_at": "2026-11-07T14:02:11.000Z",
"sent_at": "2026-11-07T14:02:12.000Z",
"finished_at": "2026-11-07T14:02:16.000Z"
}예약된 캠페인 취소
POSThttps://smsbulk.cc/rest/v1/campaigns/{id}/cancel
시작 시간까지 작동해요. 전체 비용이 하나의 원장 항목으로 크레딧에 반환되고 캠페인 상태가 취소로 변경돼요.
웹훅
콘솔에서 엔드포인트 URL을 설정하세요. message.delivered, message.failed, campaign.done에 대해 JSON을 POST하고, Send test event를 누르면 테스트도 함께 보내요. 각 이벤트는 네 개의 헤더를 가지고 있으며, 10초 이내의 2xx 응답은 수신된 것으로 간주돼요.
| 필드 | 타입 | 의미 |
|---|---|---|
| Smsbulk-Event | 헤더 | 이벤트 이름이에요. 예: message.delivered. |
| Smsbulk-Delivery | 헤더 | 배송 ID예요. 하나의 이벤트를 재시도할 때마다 동일해요. |
| Smsbulk-Timestamp | 헤더 | 이 시도가 서명된 Unix 초예요. |
| Smsbulk-Signature | 헤더 | sha256= 뒤에 타임스탬프, 줄 바꿈 및 원본 본문의 base64 HMAC-SHA256이 와요. sbw_ 비밀 키로 서명돼요. |
전송은 8번 시도돼요: 즉시, 그리고 1분, 5분, 15분, 1시간, 3시간, 6시간, 12시간 후에요. 각 시도는 콘솔 전송 로그에 HTTP 상태와 응답의 시작 부분과 함께 표시돼요.
{
"id": "dlv_3f9a1c07b2e44d58a6c1",
"event": "message.failed",
"created": "2026-11-07T14:02:16.000Z",
"data": {
"message": "M220947781305",
"campaign": "C507318264409",
"to": "+447400123456",
"country": "GB",
"status": "failed",
"reason": "not_in_service",
"segments": 1,
"price": "0.00755",
"client_ref": "order-batch-17"
}
}서명을 확인해요
import crypto from "node:crypto";
// raw = the request body exactly as received (string), secret = your sbw_ signing secret
export function verify(raw, headers, secret) {
const ts = headers["smsbulk-timestamp"];
const want = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "\n" + raw).digest("base64");
const got = headers["smsbulk-signature"] || "";
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
}import base64, hashlib, hmac, time
def verify(raw: bytes, headers, secret: str) -> bool:
ts = headers.get("Smsbulk-Timestamp", "")
mac = hmac.new(secret.encode(), ts.encode() + b"\n" + raw, hashlib.sha256).digest()
want = "sha256=" + base64.b64encode(mac).decode()
fresh = abs(time.time() - int(ts or 0)) < 300
return fresh and hmac.compare_digest(want, headers.get("Smsbulk-Signature", ""))오류 및 제한
에러는 HTTP 상태 코드와 JSON 본문으로 표시돼요: {"error":{"code":"insufficient_credit","message":"..."}}. 요청이 실패하면 요금이 청구되지 않아요.
| 필드 | 타입 | 의미 |
|---|---|---|
| 400 bad_json | 본문이 JSON이 아니에요. | |
| 401 unauthorized | 누락되었거나 잘못된 형식이거나 취소된 키예요. | |
| 402 insufficient_credit | 총 금액이 크레딧보다 많아요. 콘솔에서 충전해 주세요. | |
| 404 not_found | 이 계정에는 해당 엔드포인트, 캠페인, 메시지 또는 ISO 코드가 없어요. | |
| 409 not_scheduled | 이미 시작된 캠페인에 대해 취소 요청이 있었어요. | |
| 422 text_missing, text_too_long, too_many_segments | 메시지가 비어 있거나, 2,000자를 초과했거나, 수신자당 10 세그먼트를 초과했어요. | |
| 422 no_recipients, too_many, recipients_missing | 발송 가능한 번호가 없거나, 8,000개 이상의 번호가 있거나, to나 recipients 필드가 없어요. | |
| 422 schedule_range, bad_send_at | send_at이 5분에서 90일 사이가 아니거나 유효한 시간이 아니에요. | |
| 422 opt_out_unset | opt_out이 true지만 설정에 수신 거부 문구가 저장되지 않았어요. | |
| 429 rate_limited | 이 키로 분당 300회 이상의 요청이 있었어요. |
모든 응답에는 X-RateLimit-Limit, X-RateLimit-Remaining 및 X-RateLimit-Reset이 포함돼요.
제외된 코드
가격 책정 전에 제외된 번호는 left_out에 반환돼요. 여기에는 목록에서의 위치, 보낸 값, 코드 및 간단한 이유가 포함돼요.
| 필드 | 타입 | 의미 |
|---|---|---|
| no_country_code | +가 없고 기본 국가도 설정되지 않았어요. | |
| too_short / too_long | 8자 미만이거나 15자를 초과했어요. | |
| not_a_number | 숫자가 아닌 글자나 기호가 포함되어 있어요. | |
| nanp_length | 11자리가 아닌 +1 번호예요. | |
| not_covered | 우리 목록에 없는 목적지예요. | |
| duplicate | 같은 번호가 요청에 이미 있었어요. | |
| suppressed | 이 번호는 발송 제외 목록에 있어요. |
상태 및 이유
| 필드 | 타입 | 의미 |
|---|---|---|
| queued | 상태 | 큐에서 나가기를 기다리고 있어요. |
| sent | 상태 | 네트워크에 전달되었고, 전송 리포트를 기다리고 있어요. 대부분 몇 분 내에 리포트가 도착하지만, 일부 네트워크는 최대 24시간이 걸릴 수 있어요. |
| delivered | 상태 | 네트워크에서 휴대폰 수신을 확인했어요. |
| failed | 상태 | 전송되지 않았어요. 가격이 크레딧으로 환불되었어요. |
| canceled | 상태 | 취소해서 전송되지 않았어요. 환불되었어요. |
| 필드 | 타입 | 의미 |
|---|---|---|
| network_error | 사유 | 네트워크 오류 |
| switched_off | 사유 | 휴대폰이 꺼져 있거나 커버리지 밖이에요 |
| not_in_service | 사유 | 서비스 중이 아닌 번호예요 |
| blocked | 사유 | 네트워크 또는 수신자에 의해 차단됨 |
| landline | 사유 | 문자를 받을 수 없는 유선전화 또는 번호 |
| content_filter | 사유 | 네트워크의 콘텐츠 필터에 의해 거부됨 |
| expired | 사유 | 만료됨: 네트워크 시간 제한 전에 리포트 없음 |
| unknown | 사유 | 알 수 없는 네트워크 응답 |
| canceled | 사유 | 발송 전 취소됨 |
API에 대해 궁금한 점이 있나요? 콘솔에서 티켓을 열거나 FAQ를 읽어보세요.