인증
Notification API의 모든 요청은 API Key + HMAC-SHA256 서명으로 인증합니다. 시크릿 자체를 매 요청에 노출하는 대신 요청 내용을 서명해서 보내므로, 전송 구간에서 요청이 위·변조되거나 가로챈 요청이 재사용(리플레이)되는 것을 막을 수 있습니다.
API Key 구조
대시보드의 거버넌스 → API 키에서 발급하면 세 가지 값을 받습니다.
| 값 | 형식 | 설명 |
|---|---|---|
keyId | pk_ + 소문자 hex 40자 | 키 식별자. 서버가 어떤 키인지 찾는 데 사용합니다 |
keySecret | 소문자 hex 72자 | 키 소유 증명. X-API-Key 헤더에 keyId와 함께 보냅니다 |
hmacSecret | 소문자 hex 96자 | 요청 서명(HMAC-SHA256) 생성 전용. 헤더로 전송하지 않습니다 |
X-API-Key 헤더에는 keyId와 keySecret을 점(.)으로 이어 보냅니다. 콜론(:) 구분자도 허용됩니다.
X-API-Key: pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRETkeySecret과 hmacSecret은 발급·재발급 응답에서 단 한 번만 노출되며, 서버에는 해시/암호화된 형태로만
저장됩니다. 분실하면 복구할 수 없으므로 키를 재발급해야 합니다. 두 시크릿 모두 서버 환경변수 등
안전한 곳에 보관하고, 클라이언트 코드에 절대 포함하지 마세요.
요청 헤더 4종
모든 요청에 아래 4개 헤더가 필요합니다. 하나라도 빠지면 401 NOTI-4013으로 거부됩니다.
| 헤더 | 값 | 예시 |
|---|---|---|
X-API-Key | keyId.keySecret | pk_xxxx....YOUR_KEY_SECRET |
X-Timestamp | 요청 시각, epoch 초 단위 정수 | 1752130800 |
X-Nonce | 요청마다 새로 생성한 1회용 임의 문자열 | 4f9d2c1b8e7a6f5d4c3b2a10 |
X-Signature | canonical string의 HMAC-SHA256 hex (소문자) | 9c3f0a... |
X-Timestamp는 밀리초가 아니라 초 단위입니다. Date.now()를 그대로 쓰면 서명이 항상 실패하니
Math.floor(Date.now() / 1000)처럼 초로 변환하세요. 가장 흔한 실수입니다.
Canonical String
서명 대상 문자열은 아래 다섯 항목을 개행 문자(\n, LF)로 이은 것입니다.
METHOD
PATH
TIMESTAMP
NONCE
BODY_SHA256| 항목 | 규칙 |
|---|---|
METHOD | HTTP 메서드 대문자 (POST, GET) |
PATH | 요청 경로. 쿼리스트링이 있으면 포함 (예: /v1/notifications) |
TIMESTAMP | X-Timestamp 헤더에 넣은 값 그대로 |
NONCE | X-Nonce 헤더에 넣은 값 그대로 |
BODY_SHA256 | 전송할 요청 본문 원본 바이트의 SHA-256 hex. 본문이 없으면(GET 등) 빈 바이트의 해시 |
빈 본문의 SHA-256 해시는 항상 다음 상수입니다. GET 요청 서명 시 이 값을 사용합니다.
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855이 canonical string에 hmacSecret을 키로 HMAC-SHA256을 적용한 hex 문자열이 X-Signature입니다.
본문 해시는 전송된 원본 바이트 기준으로 검증됩니다. 서명한 뒤 JSON을 다시 직렬화하면 (공백·키 순서가 달라지면) 서명이 깨집니다. 반드시 서명에 사용한 문자열을 그대로 전송하세요.
서명 생성 예시
TypeScript (Node.js)
import { createHash, createHmac, randomBytes } from 'node:crypto'
const BASE_URL = 'https://api.posmit.io'
const API_KEY = 'pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRET'
const HMAC_SECRET = 'YOUR_HMAC_SECRET'
function signedHeaders(method: string, path: string, body: string) {
const timestamp = Math.floor(Date.now() / 1000).toString()
const nonce = randomBytes(12).toString('hex')
const bodyHash = createHash('sha256').update(body, 'utf8').digest('hex')
const canonical = [method.toUpperCase(), path, timestamp, nonce, bodyHash].join('\n')
const signature = createHmac('sha256', HMAC_SECRET).update(canonical).digest('hex')
return {
'X-API-Key': API_KEY,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
}
}
async function sendNotification() {
const path = '/v1/notifications'
const body = JSON.stringify({
channel: 'sms',
recipient: { address: '01012345678' },
content: { text: '[Posmit] 인증번호는 123456입니다.' },
})
const response = await fetch(BASE_URL + path, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...signedHeaders('POST', path, body),
},
body, // 서명에 사용한 문자열을 그대로 전송합니다
})
console.log(await response.json())
}
async function getNotification(requestId: string) {
const path = `/v1/notifications/${requestId}`
const response = await fetch(BASE_URL + path, {
// GET은 본문이 없으므로 빈 문자열로 서명합니다
headers: signedHeaders('GET', path, ''),
})
console.log(await response.json())
}curl + 서명 헬퍼 CLI
셸에서 테스트할 때는 서명 헬퍼 CLI로 헤더를 만든 뒤 curl에 붙여 넣는 흐름이 편합니다.
BODY='{"channel":"sms","recipient":{"address":"01012345678"},"content":{"text":"hello"}}'
# 1) 헤더 생성 — X-API-Key / X-Timestamp / X-Nonce / X-Signature를 출력합니다
npm run sign:hmac -- \
--method POST \
--path /v1/notifications \
--body "$BODY" \
--apiKey 'pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRET' \
--hmacSecret 'YOUR_HMAC_SECRET'
# 2) 출력된 값을 그대로 사용해 요청
curl -X POST https://api.posmit.io/v1/notifications \
-H "Content-Type: application/json" \
-H "X-API-Key: pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRET" \
-H "X-Timestamp: 1752130800" \
-H "X-Nonce: 4f9d2c1b8e7a6f5d4c3b2a10" \
-H "X-Signature: GENERATED_SIGNATURE" \
-d "$BODY"서명 헬퍼 CLI는 현재 --body ""처럼 빈 문자열 값을 허용하지 않습니다(Missing value for --body 에러).
본문이 없는 GET 요청은 위 TypeScript 예시처럼 빈 문자열('')로 직접 서명하세요.
timestamp와 nonce는 서명에 포함되므로, CLI가 출력한 X-Timestamp·X-Nonce·X-Signature 세 값을
한 세트로 사용해야 합니다. 하나라도 바꾸면 서명 검증에 실패합니다.
Timestamp · Nonce 정책
리플레이 공격을 막기 위해 두 가지 검사가 함께 동작합니다.
| 정책 | 기본값 | 동작 |
|---|---|---|
| Timestamp 허용 오차 | ±300초 (5분) | 서버 시각과의 차이가 오차를 넘으면 401 NOTI-4015 |
| Nonce 1회성 | TTL 300초 | 같은 keyId로 동일 nonce를 재사용하면 401 NOTI-4016 |
- 서버 시계가 5분 이상 어긋나 있으면 모든 요청이 실패합니다. NTP 등으로 시각을 동기화하세요.
- nonce는 keyId 단위로만 유일하면 됩니다. 다른 API Key와는 충돌하지 않습니다.
- nonce의 TTL은 timestamp 허용 오차보다 짧아지지 않도록 서버가 보장하므로, 허용 오차 안에서의 재사용은 항상 차단됩니다.
- 서명 요청이 실패했더라도 이미 사용한 nonce는 소모될 수 있습니다. 재시도 시에는 timestamp·nonce·서명을 새로 생성하세요.
검증 순서와 에러 코드
인증은 아래 순서로 검사하며, 실패하면 모두 HTTP 401 + application/problem+json으로 응답합니다.
| 순서 | 검사 | 실패 시 코드 | detail |
|---|---|---|---|
| 1 | 헤더 4종 존재 여부 | NOTI-4013 | Missing header: x-... |
| 2 | keyId로 활성 키 조회 | NOTI-4010 | Invalid API key |
| 3 | 키 만료(expiresAt) | NOTI-4011 | API key expired |
| 4 | keySecret 일치 | NOTI-4010 | Invalid API key secret |
| 5 | timestamp 허용 오차 | NOTI-4015 | Timestamp out of tolerance |
| 6 | nonce 재사용 | NOTI-4016 | Replay detected |
| 7 | HMAC 서명 일치 | NOTI-4014 | Invalid HMAC signature |
에러 응답 형식은 에러 코드 문서를 참고하세요.
스코프
API Key에는 허용된 작업 범위(스코프)가 부여됩니다. 스코프가 부족하면 HTTP 403으로 거부됩니다.
| 엔드포인트 | 필요 스코프 |
|---|---|
POST /v1/notifications | notifications:send |
POST /v1/notifications/bulk | notifications:send |
GET /v1/notifications/:requestId | notifications:read |
POST /v1/notifications/:requestId/cancel | notifications:send |
스코프를 지정하지 않고 발급한 키는 기본으로 notifications:send만 가집니다.
상태 조회까지 하려면 notifications:read를 함께 부여하세요.
키에 허용 템플릿 목록이 설정된 경우, 목록에 없는 템플릿으로 발송을 시도하면 마찬가지로 403이 반환됩니다.
키 로테이션
대시보드에서 키를 재발급(로테이션) 하면 새 keyId·keySecret·hmacSecret이 발급되고,
기존 키의 속성은 그대로 승계됩니다.
- 승계 항목: 스코프, 허용 템플릿 목록, 만료일, 키 전용 요청 한도 정책
- 기존 키는 재발급 즉시 폐기(revoked) 되어 더 이상 인증에 사용할 수 없습니다
재발급은 기존 키를 즉시 무효화하므로, 새 시크릿을 배포하기 전까지 해당 키를 쓰는 서비스의 요청이 모두 401로 실패합니다. 시크릿 유출 등 즉시 차단이 필요한 상황에 사용하세요.
운영 중 무중단으로 키를 교체하려면 다음 순서를 권장합니다.
- 같은 스코프로 새 키를 추가 발급합니다 (재발급이 아니라 신규 생성).
- 서비스에 새 키를 배포하고 정상 동작을 확인합니다.
- 기존 키를 폐기합니다.
키에 만료일(expiresAt)을 설정해 두면 주기적 교체를 강제할 수 있습니다. 만료된 키는 401 NOTI-4011로 거부됩니다.