공식 SDK 상태
Node.js · Python 패키지 준비 중
아직 배포된 공식 패키지가 없습니다(npm install 대상 없음). 아래 예제는 언어 기본 HTTP 클라이언트만 사용하므로 의존성 추가 없이 그대로 쓰실 수 있고, SDK가 나와도 호출 규약은 동일합니다.
공통 규약
- Base URL: https://api.apexstack.com/api/send/v1
- 인증 헤더: X-API-Key: axk_live_… (서버에서만 사용 — 클라이언트 코드에 넣지 마세요)
- 요청·응답 모두 JSON(UTF-8), 응답 봉투는 { success, message, data }
- 재시도는 같은 idempotencyKey로 — 중복 발송되지 않습니다
Node.js
런타임 내장 fetch 사용 (Node 18+). 별도 패키지가 필요 없습니다.
javascript
const BASE = "https://api.apexstack.com/api/send/v1";
async function call(path, { method = "GET", body } = {}) {
const res = await fetch(BASE + path, {
method,
headers: {
"X-API-Key": process.env.APEX_SEND_KEY,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok || json.success === false) {
// message 는 한국어 사유가 그대로 담겨 온다 — 로그에 남겨두면 원인 파악이 빠르다
throw new Error(`[${res.status}] ${json.message ?? "요청 실패"}`);
}
return json.data;
}
// 승인된 템플릿 조회 → id 를 하드코딩하지 않는다(재등록 시 바뀜)
const templates = await call("/templates?status=APPROVED");
const tpl = templates.find((t) => t.name === "주문 완료 안내");
// 발송
const result = await call("/messages", {
method: "POST",
body: {
channelId: "alimtalk",
templateId: String(tpl.id),
campaignName: "주문 완료 안내",
idempotencyKey: `order-${order.id}`,
recipients: [{
to: order.phone,
ref: `order-${order.id}`,
vars: { 고객명: order.customerName, 주문번호: order.code },
}],
},
});
console.log(result.campaignId, result.estimatedCost, result.balanceAfter);Python
requests 사용. 표준 라이브러리만 쓰려면 urllib.request 로 바꿔도 동일합니다.
python
import os, requests
BASE = "https://api.apexstack.com/api/send/v1"
HEADERS = {"X-API-Key": os.environ["APEX_SEND_KEY"], "Content-Type": "application/json"}
def call(path, method="GET", body=None):
res = requests.request(method, BASE + path, headers=HEADERS, json=body, timeout=15)
data = res.json()
if not res.ok or data.get("success") is False:
raise RuntimeError(f"[{res.status_code}] {data.get('message', '요청 실패')}")
return data["data"]
templates = call("/templates?status=APPROVED")
tpl = next(t for t in templates if t["name"] == "주문 완료 안내")
result = call("/messages", "POST", {
"channelId": "alimtalk",
"templateId": str(tpl["id"]),
"idempotencyKey": f"order-{order_id}",
"recipients": [{
"to": phone,
"ref": f"order-{order_id}",
"vars": {"고객명": customer_name, "주문번호": order_code},
}],
})
print(result["campaignId"], result["balanceAfter"])PHP
php
<?php
$base = "https://api.apexstack.com/api/send/v1";
$payload = json_encode([
"channelId" => "alimtalk",
"templateId" => "128",
"idempotencyKey" => "order-" . $orderId,
"recipients" => [[
"to" => $phone,
"ref" => "order-" . $orderId,
"vars" => ["고객명" => $name, "주문번호" => $code],
]],
], JSON_UNESCAPED_UNICODE);
$ch = curl_init("$base/messages");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("APEX_SEND_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $payload,
]);
$res = json_decode(curl_exec($ch), true);
if (empty($res["success"])) { throw new RuntimeException($res["message"] ?? "요청 실패"); }
echo $res["data"]["campaignId"];연동할 때 자주 걸리는 것
- templateId 하드코딩 — 템플릿이 반려되어 재등록되면 id가 바뀌어 조용히 실패합니다. 매번 /templates로 조회하거나 앱 시작 시 캐시하세요.
- 변수 누락 — 템플릿의 #{변수}를 하나라도 빠뜨리면 400입니다. /templates 응답의 변수 목록으로 검증하세요.
- 전화번호 형식 — 하이픈은 있어도 되지만 국가번호 없이 010… 형태를 권장합니다.
- 잔액 감시 — 선불이라 0이 되면 발송이 402로 막힙니다. READ_ONLY 키로 /balance를 주기 점검하세요.
- 키를 클라이언트에 두지 않기 — 브라우저·모바일 앱에서 직접 호출하면 키가 그대로 노출됩니다. 반드시 서버 경유로.