Skip to Content
개발자인증

인증

Notification API의 모든 요청은 API Key + HMAC-SHA256 서명으로 인증합니다. 시크릿 자체를 매 요청에 노출하는 대신 요청 내용을 서명해서 보내므로, 전송 구간에서 요청이 위·변조되거나 가로챈 요청이 재사용(리플레이)되는 것을 막을 수 있습니다.

API Key 구조

대시보드의 거버넌스 → API 키에서 발급하면 세 가지 값을 받습니다.

형식설명
keyIdpk_ + 소문자 hex 40자키 식별자. 서버가 어떤 키인지 찾는 데 사용합니다
keySecret소문자 hex 72자키 소유 증명. X-API-Key 헤더에 keyId와 함께 보냅니다
hmacSecret소문자 hex 96자요청 서명(HMAC-SHA256) 생성 전용. 헤더로 전송하지 않습니다

X-API-Key 헤더에는 keyIdkeySecret을 점(.)으로 이어 보냅니다. 콜론(:) 구분자도 허용됩니다.

X-API-Key: pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRET

keySecrethmacSecret은 발급·재발급 응답에서 단 한 번만 노출되며, 서버에는 해시/암호화된 형태로만 저장됩니다. 분실하면 복구할 수 없으므로 키를 재발급해야 합니다. 두 시크릿 모두 서버 환경변수 등 안전한 곳에 보관하고, 클라이언트 코드에 절대 포함하지 마세요.

요청 헤더 4종

모든 요청에 아래 4개 헤더가 필요합니다. 하나라도 빠지면 401 NOTI-4013으로 거부됩니다.

헤더예시
X-API-KeykeyId.keySecretpk_xxxx....YOUR_KEY_SECRET
X-Timestamp요청 시각, epoch 초 단위 정수1752130800
X-Nonce요청마다 새로 생성한 1회용 임의 문자열4f9d2c1b8e7a6f5d4c3b2a10
X-Signaturecanonical string의 HMAC-SHA256 hex (소문자)9c3f0a...

X-Timestamp는 밀리초가 아니라 단위입니다. Date.now()를 그대로 쓰면 서명이 항상 실패하니 Math.floor(Date.now() / 1000)처럼 초로 변환하세요. 가장 흔한 실수입니다.

Canonical String

서명 대상 문자열은 아래 다섯 항목을 개행 문자(\n, LF)로 이은 것입니다.

METHOD PATH TIMESTAMP NONCE BODY_SHA256
항목규칙
METHODHTTP 메서드 대문자 (POST, GET)
PATH요청 경로. 쿼리스트링이 있으면 포함 (예: /v1/notifications)
TIMESTAMPX-Timestamp 헤더에 넣은 값 그대로
NONCEX-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-4013Missing header: x-...
2keyId로 활성 키 조회NOTI-4010Invalid API key
3키 만료(expiresAt)NOTI-4011API key expired
4keySecret 일치NOTI-4010Invalid API key secret
5timestamp 허용 오차NOTI-4015Timestamp out of tolerance
6nonce 재사용NOTI-4016Replay detected
7HMAC 서명 일치NOTI-4014Invalid HMAC signature

에러 응답 형식은 에러 코드 문서를 참고하세요.

스코프

API Key에는 허용된 작업 범위(스코프)가 부여됩니다. 스코프가 부족하면 HTTP 403으로 거부됩니다.

엔드포인트필요 스코프
POST /v1/notificationsnotifications:send
POST /v1/notifications/bulknotifications:send
GET /v1/notifications/:requestIdnotifications:read
POST /v1/notifications/:requestId/cancelnotifications:send

스코프를 지정하지 않고 발급한 키는 기본으로 notifications:send만 가집니다. 상태 조회까지 하려면 notifications:read를 함께 부여하세요.

키에 허용 템플릿 목록이 설정된 경우, 목록에 없는 템플릿으로 발송을 시도하면 마찬가지로 403이 반환됩니다.

키 로테이션

대시보드에서 키를 재발급(로테이션) 하면 새 keyId·keySecret·hmacSecret이 발급되고, 기존 키의 속성은 그대로 승계됩니다.

  • 승계 항목: 스코프, 허용 템플릿 목록, 만료일, 키 전용 요청 한도 정책
  • 기존 키는 재발급 즉시 폐기(revoked) 되어 더 이상 인증에 사용할 수 없습니다

재발급은 기존 키를 즉시 무효화하므로, 새 시크릿을 배포하기 전까지 해당 키를 쓰는 서비스의 요청이 모두 401로 실패합니다. 시크릿 유출 등 즉시 차단이 필요한 상황에 사용하세요.

운영 중 무중단으로 키를 교체하려면 다음 순서를 권장합니다.

  1. 같은 스코프로 새 키를 추가 발급합니다 (재발급이 아니라 신규 생성).
  2. 서비스에 새 키를 배포하고 정상 동작을 확인합니다.
  3. 기존 키를 폐기합니다.

키에 만료일(expiresAt)을 설정해 두면 주기적 교체를 강제할 수 있습니다. 만료된 키는 401 NOTI-4011로 거부됩니다.

Last updated on