빠른 시작
- 1. 키를 만드세요. 워크스페이스에서 API 및 웹훅을 여세요. 키는 첫 충전이 반영된 뒤 열리고, 키마다 한 번만 표시되니 복사하거나 텍스트 파일로 저장하세요.
- 2. 비용을 확인하세요. 동일한 본문을
/costs에 POST하여 수신자, 세그먼트 및 총 비용을 확인하세요. 발송은 되지 않아요. - 3. 발송하세요.
/sends에 POST하세요. 응답에는 발송 ID가 포함되며, 메시지는 즉시 또는send_at에 발송돼요.
curl https://sendsmsonline.cc/api/v1/sends \
-H "Authorization: Bearer $SSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "recipients": [ { "to": "+447700900461", "fields": { "first_name": "Tom" } }, { "to": "+12015550142", "fields": { "first_name": "Ava" } } ], "text": "Hi {{first_name}}, your table is ready at 7:30.", "name": "Friday bookings", "client_ref": "bookings-1107" }'const res = await fetch("https://sendsmsonline.cc/api/v1/sends", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SSO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
recipients: [{ to: "+447700900461", fields: { first_name: "Tom" } }],
text: "Hi {{first_name}}, your table is ready at 7:30.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data.send.id, data.send.cost_usd);import os, requests
r = requests.post(
"https://sendsmsonline.cc/api/v1/sends",
headers={"Authorization": f"Bearer {os.environ['SSO_API_KEY']}"},
json={
"recipients": [{"to": "+447700900461", "fields": {"first_name": "Tom"}}],
"text": "Hi {{first_name}}, your table is ready at 7:30.",
},
timeout=30,
)
data = r.json()
if r.status_code >= 400:
raise RuntimeError(data["error"]["message"])
print(data["send"]["id"], data["send"]["cost_usd"])<?php
$ch = curl_init("https://sendsmsonline.cc/api/v1/sends");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SSO_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"recipients" => [["to" => "+447700900461", "fields" => ["first_name" => "Tom"]]],
"text" => "Hi {{first_name}}, your table is ready at 7:30.",
]),
]);
$data = json_decode(curl_exec($ch), true);
echo $data["send"]["id"] ?? $data["error"]["message"];인증
Authorization 헤더로 키를 보내세요: Authorization: Bearer sso_live_.... 키는 49자이고 sso_live_로 시작해요. 계정당 최대 5개까지 만들 수 있고, 워크스페이스에서 폐기하면 바로 작동이 멈춰요. 가격 목록은 공개라 키가 필요 없어요.
오류
오류는 HTTP 상태 코드와 안정적인 코드, 사람에게 보여줄 수 있는 문장이 포함된 JSON 본문을 사용해요. 요청이 실패하면 비용이 청구되지 않아요.
HTTP/1.1 402 Payment Required
{ "error": { "code": "insufficient_credit", "message": "Short by $3.20. Add funds to send.", "total": "8.62480", "credit": "5.42000" } }| 필드 | 타입 | 의미 |
|---|---|---|
| 400 | invalid_json, text_missing, recipients_missing, too_many_recipients, too_many_segments, schedule_range, bad_send_at | 요청에 변경이 필요해요. |
| 401 | unauthorized | 키가 없거나, 틀렸거나, 폐기됐어요. |
| 402 | insufficient_credit | 총 비용에 대한 잔액이 부족해요. |
| 404 | not_found | 해당 발송, 메시지, 국가 또는 엔드포인트가 없어요. |
| 409 | not_cancelable | 발송이 이미 시작되었어요. |
| 429 | rate_limited | 1분에 240건을 넘는 요청이에요. |
제한
- 키당 1분에 240개의 요청이 가능하며, X-RateLimit-Limit, X-RateLimit-Remaining 및 X-RateLimit-Reset(유닉스 초)에 표시돼요.
- 요청당 5,000개의 번호, 텍스트당 2,000자, 메시지당 10개의 세그먼트로 제한돼요.
- 계정당 1분에 최대 300개의 메시지가 발송돼요.
- send_at은 10분 후부터 30일 후까지 정할 수 있어요.
가격
GET/api/v1/prices
GET/api/v1/prices/{country}
각 국가별 메시지(한 세그먼트)당 가격을 5자리 소수점 USD로 표시해요. 키가 필요하지 않아요.
{ "price": { "country": "GB", "name": "United Kingdom", "dial_code": "+44", "price_usd": "0.00868" }, "updated": "2026-11-07" }계정 잔액
GET/api/v1/account
{ "account": "SO8K2M4Q7X9TBA", "credit_usd": "41.27660", "currency": "USD" }비용 예측
POST/api/v1/costs
발송과 동일한 본문을 받아 비용을 반환하며, 실제로 발송하거나 요금을 청구하지 않아요. 각 수신자의 병합된 텍스트가 세그먼트를 결정하므로 총 비용이 정확해요.
{
"recipients": 2, "segments_min": 1, "segments_max": 1, "encoding": "gsm7", "messages_billed": 2,
"total_usd": "0.01426", "credit_usd": "41.27660", "enough_credit": true,
"countries": [ { "country": "GB", "recipients": 1, "price_usd": "0.00868", "subtotal_usd": "0.00868" },
{ "country": "US", "recipients": 1, "price_usd": "0.00558", "subtotal_usd": "0.00558" } ],
"dropped": [], "too_long": null
}발송 생성
POST/api/v1/sends
하나의 텍스트를 하나 또는 여러 번호로 발송해요. 발송이 승인되면 전체 발송 비용이 잔액에서 차감되며, 실패한 메시지는 하나씩 환불돼요.
| 필드 | 타입 | 의미 |
|---|---|---|
| to | string or array | 국제 형식(+447700900461)의 번호를 사용해요. 이 필드나 recipients 필드를 사용하세요. |
| recipients | array | 병합 필드를 위한 to와 fields가 포함된 객체예요: { "to": "+1...", "fields": { "first_name": "Ava" } }. |
| text | string | 메시지는 최대 2,000자까지 가능해요. {{field}}은 수신자별로 대체되며, 누락된 값은 빈 값이 돼요. |
| send_at | string | 선택적으로 ISO 8601 형식의 시간을 오프셋 또는 Z로 설정할 수 있어요. 설정하지 않으면 즉시 발송이 시작돼요. |
| name | string | 워크스페이스에 표시되는 선택적 라벨이에요(80자). |
| client_ref | string | 웹훅에 반환되는 선택적 참조 값이에요(64자). |
| opt_out | boolean | 설정에 저장된 수신 거부 안내 문구를 텍스트 끝에 추가해요. |
| default_country | string | 국가 코드 없이 작성된 번호에 사용되는 ISO 코드예요. |
HTTP/1.1 201 Created
{
"send": { "id": "CA7Q29XMAB4DFK", "status": "queued", "source": "api", "recipients": 2, "segments_max": 1,
"encoding": "gsm7", "cost_usd": "0.01426", "refunded_usd": "0.00000", "client_ref": "bookings-1107",
"send_at": null, "created_at": "2026-11-07T10:12:03.120Z", "started_at": null, "finished_at": null },
"dropped": [ { "value": "+4412", "code": "too_short", "message": "Too short" } ]
}발송할 수 없는 번호는 dropped에 나열되며 요금이 청구되지 않아요. 코드: no_country_code, too_short, too_long, not_a_number, wrong_length, not_covered, duplicate 및 suppressed(발송 제외 목록에 있는 경우).
발송 가져오기 또는 목록 보기
GET/api/v1/sends/{id}
GET/api/v1/sends?limit=25&before={id}
단일 발송에는 메시지 상태별 counts가 포함돼요. 목록은 최신순으로 정렬되며, 다음 페이지를 보려면 next_before를 before로 전달하세요. 워크스페이스에서의 발송도 여기에 표시되며, source: "web"로 표시돼요.
메시지
GET/api/v1/sends/{id}/messages?status=failed&limit=100
GET/api/v1/messages/{id}
{ "message": { "id": "MS3D8RW0K2QJ7N", "send_id": "CA7Q29XMAB4DFK", "to": "+447700900461", "country": "GB",
"segments": 1, "price_usd": "0.00868", "status": "delivered", "reason": null, "reason_text": null,
"created_at": "2026-11-07T10:12:03.120Z", "sent_at": "2026-11-07T10:12:04.010Z", "done_at": "2026-11-07T10:12:09.400Z" } }발송 취소
POST/api/v1/sends/{id}/cancel
시작하지 않은 예약 발송만 취소할 수 있어요. 전체 비용이 잔액으로 환불되며 응답에 refunded_usd가 포함돼요.
웹훅
워크스페이스(API 및 웹훅)에 https URL을 설정하세요. 메시지가 전달되거나 실패할 때 이벤트를 POST로 보내요. API와 워크스페이스에서의 발송 모두에 적용돼요. 10초 이내에 2xx 상태로 응답하세요. 실패한 전송은 총 7번 재시도돼요: 즉시, 1분 후, 5분 후, 30분 후, 1시간 후, 2시간 후, 3시간 후. 워크스페이스는 각 시도와 그 상태 및 응답을 표시해요.
POST https://your-server.example/sms-events
X-SSO-Event: message.delivered
X-SSO-Delivery: evt_5a0c9e31d2b7f4a68c
X-SSO-Timestamp: 1794132729
X-SSO-Signature: sha256=8f1c...e42a
{ "id": "evt_5a0c9e31d2b7f4a68c", "event": "message.delivered", "created": "2026-11-07T10:12:09.000Z",
"data": { "message": { "id": "MS3D8RW0K2QJ7N", "send_id": "CA7Q29XMAB4DFK", "to": "+447700900461", "country": "GB",
"segments": 1, "price_usd": "0.00868", "status": "delivered", "reason": null, "reason_text": null,
"client_ref": "bookings-1107", "created_at": "...", "sent_at": "...", "done_at": "..." } } }서명은 timestamp + "." + raw body를 서명 비밀 키(sso_whk_로 시작)로 HMAC-SHA256한 값이에요. 파싱 전에 원본 본문으로 확인하고, 5분보다 오래된 타임스탬프는 거부하고, 전송 ID로 중복을 걸러 내세요.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string), before JSON.parse
export function verify(rawBody, headers, secret) {
const ts = headers["x-sso-timestamp"];
const sig = headers["x-sso-signature"] || "";
const want = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want));
}import hashlib, hmac, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers.get("X-SSO-Timestamp", "")
sig = headers.get("X-SSO-Signature", "")
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256)
want = "sha256=" + mac.hexdigest()
fresh = ts.isdigit() and abs(time.time() - int(ts)) < 300
return fresh and hmac.compare_digest(sig, want)<?php
function verify(string $rawBody, string $ts, string $sig, string $secret): bool {
$want = "sha256=" . hash_hmac("sha256", $ts . "." . $rawBody, $secret);
return ctype_digit($ts) && abs(time() - (int)$ts) < 300 && hash_equals($want, $sig);
}이벤트: message.delivered, message.failed, 그리고 Send test event 버튼의 test가 있어요.
상태 및 이유
| 필드 | 타입 | 의미 |
|---|---|---|
| queued | status | 발송 대기열에서 기다리고 있어요. |
| sent | status | 네트워크로 전달되었고, 전송 리포트를 기다리고 있어요. 대부분의 리포트는 몇 분 내에 도착하지만, 일부 네트워크는 최대 24시간이 걸릴 수 있어요. |
| delivered | status | 네트워크에서 휴대폰이 메시지를 받았다고 확인했어요. |
| failed | status | 전송되지 않았어요. 가격이 잔액으로 환불되었어요. |
| canceled | status | 취소해서 전송되지 않았어요. 환불되었어요. |
실패 이유 (reason 필드):
| 필드 | 타입 | 의미 |
|---|---|---|
| network_error | reason | 네트워크 오류 |
| absent | reason | 휴대폰이 꺼져 있거나 서비스 영역 밖 |
| not_in_service | reason | 서비스 중이 아닌 번호 |
| blocked | reason | 네트워크나 수신자에 의해 차단됨 |
| landline | reason | SMS를 받을 수 없는 유선전화 또는 번호 |
| content_filter | reason | 네트워크의 콘텐츠 필터에 의해 거부됨 |
| expired | reason | 만료: 네트워크의 시간 제한 내에 리포트 없음 |
| canceled | reason | 전송 전 취소됨 |
| unknown | reason | 알 수 없는 네트워크 응답 |