대량 SMS API 레퍼런스

BulkSMSOnline REST API로 직접 만든 소프트웨어에서 227개 국가에 문자 메시지를 보낼 수 있고, 요금, 세그먼트 규칙, 예약 발송, 환불은 콘솔과 같아요. 요청과 응답은 HTTPS 위의 JSON이고, 모든 최종 전송 상태는 서명된 웹훅으로 서버에 전달할 수 있어요.

기본 URLhttps://bulksmsonline.cc/api/v1

개요

쇼핑몰의 주문 업데이트, 로그인 양식의 코드, 예약 도구의 알림처럼 내 시스템이 메시지를 발생시킬 때 API를 사용하세요. 번호 최대 10,000개를 담은 POST /v1/messages 요청 하나가 캠페인 하나를 만들고, 잔액에서 요금을 차감하고, 모든 메시지를 대기열에 올려요. API로 만든 캠페인도 콘솔에 전송 리포트와 함께 나타나요.

  • 모든 금액은 소수점 다섯 자리의 문자열로 된 미국 달러예요. 예: "0.00534".
  • 전화번호는 국제 형식이며 앞의 +는 있어도 없어도 돼요. 공백과 하이픈은 제거돼요.
  • 시각은 UTC 기준 ISO 8601이에요. 예: 2026-12-01T09:00:00Z.
  • 메시지와 캠페인 ID는 MSG-XXXX-XXXX-XXXX, CMP-XXXX-XXXX-XXXX처럼 생겼어요.

인증

콘솔의 API에서 API 키를 만드세요(키는 첫 충전이 반영된 뒤에 열려요). 모든 요청의 Authorization 헤더에 키를 담아 보내세요. 키는 bso_api_ 뒤에 36자가 이어지는 형태이고, 한 번만 표시되니 서버에 비밀 값으로 보관하세요. 새 키를 만들면 이전 키는 즉시 교체돼요.

Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123
Content-Type: application/json

한도

요청키당 분당 300건, 초과하면 429 rate_limited로 응답해요
요청당 번호 수10,000
메시지 문구2,000자 그대로, 최대 10세그먼트
예약 발송send_at: 5분 후부터 60일 후까지
발송 속도대기열당 분당 400건
웹훅 재시도약 22시간 동안 9번 시도

메시지 발송

POST/v1/messages

하나 이상의 번호에 문자 하나를 보내요. 전체 비용은 요청이 접수될 때 잔액에서 차감되고, 전송되지 못한 메시지는 이후에 환불돼요. 응답은 상태 201과 모든 메시지가 담긴 캠페인이에요.

필드설명
to필수국제 형식의 번호 하나 또는 최대 10,000개의 번호 배열이에요. 중복 번호는 한 번만 발송되고, 읽을 수 없는 번호는 skipped에 나열돼요.
text필수메시지이며 최대 2,000자, 10세그먼트예요. GSM-7 문구는 SMS당 160자, Unicode는 70자예요.
send_at선택발송을 예약하는 ISO 8601 시각으로, 지금부터 5분 후에서 60일 후까지예요. 생략하면 바로 보내요.
opt_out선택true로 설정하면 콘솔 설정의 수신 거부 안내 문구가 마지막에 새 줄로 추가돼요.
client_ref선택직접 정하는 참조값으로, 문자와 숫자와 _ . : -를 합쳐 최대 64자예요. 리포트와 웹훅에 그대로 돌아와요.
curl -X POST https://bulksmsonline.cc/api/v1/messages \
  -H "Authorization: Bearer $BSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":["+447700900412","+33612345678"],"text":"Order 4521 has shipped. Track it at shop.example/t/4521","client_ref":"order-4521"}'
HTTP/1.1 201 Created
{
  "id": "CMP-7KQ2-M9XA-4HTD",
  "status": "sending",
  "channel": "api",
  "recipients": 2,
  "segments": 1,
  "encoding": "GSM-7",
  "cost": "0.02158",
  "refunded": "0.00000",
  "client_ref": "order-4521",
  "send_at": null,
  "counts": { "queued": 2, "submitted": 0, "delivered": 0, "failed": 0, "canceled": 0 },
  "skipped": [],
  "balance": "118.43120",
  "messages": [
    { "id": "MSG-3RTW-81KD-ZQ0P", "to": "+447700900412", "country": "GB", "segments": 1, "price": "0.00834", "status": "queued" },
    { "id": "MSG-5HNA-4C2E-7YVB", "to": "+33612345678", "country": "FR", "segments": 1, "price": "0.01324", "status": "queued" }
  ]
}

발송 견적

POST/v1/quote

/v1/messages와 같은 본문을 받아서, 발송하거나 비용을 차감하지 않고 정확한 비용, 세그먼트, 인코딩을 돌려줘요. 사용자가 확정하기 전에 직접 만든 화면에서 요금을 보여줄 때 사용하세요.

{
  "recipients": 2,
  "skipped": [],
  "segments": 1,
  "encoding": "GSM-7",
  "cost": "0.02158",
  "balance": "118.45278",
  "countries": [ { "country": "GB", "recipients": 1, "cost": "0.00834" }, { "country": "FR", "recipients": 1, "cost": "0.01324" } ]
}

잔액

GET/v1/balance

{ "balance": "118.43120", "currency": "USD" }

요금

GET/v1/rates

GET/v1/rates/{country}

모든 국가 또는 두 글자 ISO 국가 코드 하나에 대한 SMS 세그먼트 1건의 요금이에요. 이 두 엔드포인트는 공개되어 있어서 키가 필요 없어요.

{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }

메시지 상태

GET/v1/messages/{id}

{
  "id": "MSG-3RTW-81KD-ZQ0P",
  "campaign_id": "CMP-7KQ2-M9XA-4HTD",
  "to": "+447700900412",
  "country": "GB",
  "segments": 1,
  "price": "0.00834",
  "status": "delivered",
  "error": null,
  "created_at": "2026-11-07T09:00:02Z",
  "submitted_at": "2026-11-07T09:00:03Z",
  "final_at": "2026-11-07T09:00:08Z"
}

캠페인

GET/v1/campaigns/{id}

GET/v1/campaigns/{id}/messages?offset=0&limit=100

첫 번째는 상태별 건수가 담긴 캠페인을 돌려주고, 두 번째는 해당 캠페인의 메시지를 페이지당 최대 1,000건씩 보여줘요. 콘솔에서 보낸 캠페인도 페이지에 표시된 ID로 같은 방식으로 조회할 수 있어요.

예약 발송 취소

POST/v1/campaigns/{id}/cancel

send_at으로 만들어졌고 아직 시작하지 않은 캠페인을 중지해요. 모든 메시지가 취소되고 비용 전액이 잔액으로 돌아와요. 이미 발송 중인 캠페인은 409 not_scheduled로 응답해요.

전송 웹훅

콘솔의 API에서 HTTPS URL을 추가하면 각 최종 상태(delivered 또는 failed)가 POST 요청으로 전달돼요. 8초 안에 아무 2xx 상태로 응답하세요. 그 밖의 응답과 시간 초과는 20초, 1분, 5분, 15분 뒤에, 이어서 1시간, 3시간, 6시간, 12시간 뒤에 다시 시도하며 총 9번이에요. 같은 이벤트가 두 번 도착할 수 있으니 event_id로 한 번만 처리하세요.

POST https://your-server.example/sms-status
Content-Type: application/json
X-BulkSMSOnline-Signature: t=1794042008,v1=5f0c2a...e91b

{
  "type": "message.status",
  "event_id": "ev_8f1c22d0a4b97e63c1f0",
  "sent_at": "2026-11-07T09:00:09Z",
  "data": {
    "message_id": "MSG-3RTW-81KD-ZQ0P",
    "campaign_id": "CMP-7KQ2-M9XA-4HTD",
    "to": "+447700900412",
    "country": "GB",
    "status": "delivered",
    "error": null,
    "segments": 1,
    "price": "0.00834",
    "currency": "USD",
    "client_ref": "order-4521",
    "final_at": "2026-11-07T09:00:08Z"
  }
}

서명 검증

헤더에는 Unix 시각 t와 서명 v1이 담겨 있어요. v1은 웹훅 시크릿으로 문자열 "t.raw_body"에 HMAC-SHA256을 계산한 hex 값이에요. JSON을 파싱하기 전에 원본 본문으로 계산하고, 일정한 시간으로 비교하고, 5분보다 오래된 이벤트는 거부하세요.

import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const want = crypto.createHmac("sha256", secret).update(m[1] + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}

상태

queued접수되어 대기열에서 나가기를 기다리는 중
submitted목적지 통신망에 전달됨
delivered휴대폰이 수신을 확인함(최종)
failed전달하지 못했고 요금이 환불됨(최종)
canceled발송 전에 취소된 예약 캠페인의 일부(최종)

캠페인 상태는 scheduled, sending, complete, canceled 중 하나예요.

오류

오류는 HTTP 상태와 JSON 본문으로 응답해요: { "error": { "code": "insufficient_balance", "message": "..." } }. 요청이 오류로 응답되면 요금은 부과되지 않아요.

HTTP코드의미
400invalid_request필드가 없거나 올바르지 않아요. 메시지에 어느 필드인지 나와요.
400invalid_json본문이 JSON이 아니에요.
400no_valid_numbers읽을 수 있는 번호가 하나도 없어요.
401unauthorized키가 없거나, 형식이 잘못되었거나, 교체되었어요.
402insufficient_balance잔액이 비용에 부족해요. 충전한 뒤 다시 보내세요.
404not_found알 수 없는 ID 또는 엔드포인트예요.
409not_scheduled아직 시작하지 않은 예약 캠페인만 취소할 수 있어요.
413too_many_recipients요청 하나에 번호가 10,000개를 넘어요.
429rate_limited1분 동안 요청이 300건을 넘어요.
500server_error요청을 다시 시도하세요. 요금은 부과되지 않았어요.

콘솔에서 API 키 받기

계정을 만들고, 첫 충전을 하고, API에서 키를 생성하세요. 1분 뒤에 첫 요청을 보낼 수 있어요.

계정 만들기
대량 SMS API 레퍼런스: REST 엔드포인트와 웹훅 | BulkSMSOnline