알림 발송 API
알림 발송의 핵심 엔드포인트 4종을 다룹니다. 모든 요청은 인증에서 설명한
HMAC 서명 헤더 4종이 필요하며, 모든 2xx 응답은 { "data": ..., "meta": { "timestamp": ... } }
봉투(envelope)로 감싸 반환됩니다.
| 엔드포인트 | 설명 | 필요 스코프 |
|---|---|---|
POST /v1/notifications | 단건 발송 | notifications:send |
POST /v1/notifications/bulk | 대량 발송 (최대 500건) | notifications:send |
GET /v1/notifications/:requestId | 상태·이력 조회 | notifications:read |
POST /v1/notifications/:requestId/cancel | 발송 취소 | notifications:send |
요청 본문에 아래 스키마에 없는 필드가 포함되면 400 에러로 거부됩니다. 본문 크기 제한은 1MB입니다.
상태 라이프사이클
발송은 비동기로 처리됩니다. 발송 요청(request)의 상태는 다음 순서로 전이됩니다.
queued → processing → sent (성공)
→ failed (모든 시도 실패)
└─ 종료 전 어느 시점이든 → canceled (취소 API 호출 시)| 상태 | 의미 |
|---|---|
queued | 접수 완료, 발송 대기 중. 예약 발송도 예약 시각 전까지 queued입니다 |
processing | 공급자를 통해 발송 시도 중 |
sent | 발송 성공 (종료 상태) |
failed | 재시도를 포함한 모든 시도 실패 (종료 상태) |
canceled | 취소 API로 취소됨 (종료 상태) |
조회 응답에는 요청 상태 외에 큐 잡(job) 상태와 공급자별 시도(attempt) 이력이 함께 담깁니다.
- 잡 상태:
scheduledqueuedprocessingretryingdead_lettercompletedfailedcanceled - 시도 상태:
successfailure— 폴백이 동작하면 한 요청에 여러 시도가 쌓입니다
상태 변화를 실시간으로 받으려면 웹훅을 사용하세요.
단건 발송
notifications:send요청 필드
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
channel | string | ✅ | 발송 채널. 아래 채널 표 참고 |
recipient.address | string | ✅ | 수신자 주소 (채널별 의미가 다름). 최대 320자 |
recipient.name | string | – | 수신자 이름. 최대 120자 |
content.text | string | △ | 본문 텍스트. templateCode가 없으면 필수 |
content.title | string | – | 제목 (이메일 제목, 푸시 타이틀 등). 최대 200자 |
content.html | string | – | HTML 본문 (이메일 등) |
content.templateCode | string | – | 템플릿 코드. 최대 120자. 템플릿 발송 참고 |
content.templateVariables | object | – | 템플릿 변수 (string → string 맵) |
idempotencyKey | string | – | 중복 발송 방지 키. 최대 120자. 멱등성 참고 |
scheduledAt | string | – | 예약 발송 시각. ISO 8601 (T 구분자 필수), 미래 시각만 허용 |
preferredProviderAccountIds | string[] | – | 우선 시도할 공급자 계정 ID(UUID). 최대 10개 |
metadata | object | – | 임의 메타데이터 (string → string 맵) |
채널
channel에 지정할 수 있는 값은 8종이며, 채널에 따라 recipient.address의 의미가 다릅니다.
| channel | 채널 | recipient.address의 의미 |
|---|---|---|
sms | SMS 문자 | 수신자 전화번호 |
kakao_alimtalk | 카카오 알림톡 | 수신자 전화번호 |
email | 이메일 | 수신자 이메일 주소 |
push_fcm | FCM 푸시 | FCM 디바이스 토큰 |
slack_webhook | Slack (Incoming Webhook) | 발송에는 사용되지 않음 (아래 참고) |
discord_webhook | Discord (Webhook) | 발송에는 사용되지 않음 (아래 참고) |
slack_bot | Slack (Bot) | 지원 준비 중 |
discord_bot | Discord (Bot) | 지원 준비 중 |
slack_webhook·discord_webhook 채널의 실제 전송 대상은 공급자 계정에 등록된 웹훅 URL입니다.
recipient.address는 발송에 쓰이지 않지만 필수 필드이므로, "ops-alerts"처럼 조회할 때
알아볼 수 있는 식별용 값을 넣으세요.
채널별 요청 예시
SMS
{
"channel": "sms",
"recipient": { "address": "01012345678" },
"content": { "text": "[Posmit] 인증번호는 123456입니다." }
}이메일 — title이 메일 제목이 되고, html이 있으면 HTML 메일로 발송됩니다.
{
"channel": "email",
"recipient": { "address": "user@example.com", "name": "홍길동" },
"content": {
"title": "가입을 환영합니다",
"text": "홍길동님, 가입이 완료되었습니다.",
"html": "<h1>환영합니다</h1><p>홍길동님, 가입이 완료되었습니다.</p>"
}
}카카오 알림톡 — 검수 승인된 알림톡 템플릿과 연결된 템플릿 코드로 발송합니다.
{
"channel": "kakao_alimtalk",
"recipient": { "address": "01012345678" },
"content": {
"templateCode": "order_complete",
"templateVariables": { "userName": "홍길동", "orderNo": "A20260710-001" }
}
}FCM 푸시
{
"channel": "push_fcm",
"recipient": { "address": "FCM_DEVICE_TOKEN" },
"content": { "title": "새 메시지", "text": "새 메시지가 도착했습니다." }
}Slack 웹훅
{
"channel": "slack_webhook",
"recipient": { "address": "ops-alerts" },
"content": { "text": ":rotating_light: 결제 실패율이 5%를 넘었습니다." }
}Discord 웹훅
{
"channel": "discord_webhook",
"recipient": { "address": "deploy-log" },
"content": { "text": "v2.4.1 배포가 완료되었습니다." }
}템플릿 발송
content.templateCode를 지정하면 대시보드에 등록된 템플릿으로 발송합니다. 해석 규칙은 다음과 같습니다.
- 워크스페이스 + 템플릿 코드 + 채널이 일치하고 활성화된 템플릿을 찾습니다. 없으면 400.
- API Key에 허용 템플릿 목록이 설정되어 있고 그 안에 없으면 403.
- 템플릿의 발행(published)된 최신 버전을 사용합니다. 발행 버전이 없으면 400.
- 요청에
content.text·title·html을 함께 보내면 해당 필드는 템플릿 값을 덮어씁니다. - 본문의
{{변수명}}자리를templateVariables의 값으로 치환합니다. 변수명은 영숫자와 언더스코어만 허용되며, 전달하지 않은 변수는 빈 문자열로 치환됩니다. - 카카오 알림톡 템플릿은 검수 상태가 승인(APPROVED)이어야 합니다. 아니면 400.
예를 들어 템플릿 본문이 {{userName}}님, 주문 {{orderNo}}이 완료되었습니다.라면:
{
"channel": "kakao_alimtalk",
"recipient": { "address": "01012345678" },
"content": {
"templateCode": "order_complete",
"templateVariables": { "userName": "홍길동", "orderNo": "A20260710-001" }
}
}멱등성 (idempotencyKey)
네트워크 오류로 같은 요청을 재시도할 때 중복 발송을 막으려면 idempotencyKey를 지정하세요.
- 중복 판정 범위는 워크스페이스 단위입니다. 같은 키로 이미 접수된 요청이 있으면 새로 만들지 않고
기존 요청을 반환하며, 응답에
"deduplicated": true가 표시됩니다. - 이때
requestId·status·scheduledAt은 기존 요청의 현재 값입니다. 이미 발송이 끝났다면status가sent로 올 수도 있습니다.
{
"channel": "sms",
"recipient": { "address": "01012345678" },
"content": { "text": "[Posmit] 인증번호는 123456입니다." },
"idempotencyKey": "signup-otp-user123-20260710"
}예약 발송 (scheduledAt)
scheduledAt에 미래의 ISO 8601 시각을 지정하면 해당 시각에 발송됩니다.
{
"channel": "email",
"recipient": { "address": "user@example.com" },
"content": { "title": "내일 오전 이벤트 안내", "text": "..." },
"scheduledAt": "2026-07-11T00:00:00.000Z"
}- 날짜와 시각 사이에
T구분자가 있는 엄격한 ISO 8601 형식이어야 합니다. 파싱할 수 없으면 400. - 과거·현재 시각이면 400 (
scheduledAt must be in the future). - 예약된 요청의 상태는 발송 시각 전까지
queued이며, 그 사이에 취소할 수 있습니다.
응답 — 201 Created
{
"data": {
"status": "queued",
"requestId": "9b2f1c3e-8a4d-4c6e-b7f0-1234567890ab",
"queueJobId": "1234",
"deduplicated": false,
"scheduledAt": null
},
"meta": { "timestamp": "2026-07-10T09:00:00.000Z" }
}| 필드 | 타입 | 설명 |
|---|---|---|
status | string | 요청 상태. 신규 접수 시 항상 queued, 중복(dedup) 시 기존 요청의 현재 상태 |
requestId | string (UUID) | 발송 요청 ID. 조회·취소에 사용합니다 |
queueJobId | string 또는 null | 내부 큐 잡 ID. 중복 응답에서 기존 잡이 없으면 null |
deduplicated | boolean | idempotencyKey 중복으로 기존 요청이 반환되었는지 여부 |
scheduledAt | string 또는 null | 예약 발송 시각. 즉시 발송이면 null |
주요 에러
| HTTP | 발생 조건 |
|---|---|
| 400 | 필드 검증 실패, 정의되지 않은 필드 포함, content.text 누락, 템플릿 없음/발행 버전 없음, scheduledAt 형식·과거 시각 |
| 401 | 인증 실패 — 인증 참고 |
| 403 | 스코프 부족, 허용되지 않은 템플릿 사용 |
| 429 | 요청 한도 초과 — 요청 한도 참고 |
대량 발송
notifications:send한 번의 요청으로 최대 500건을 접수합니다. items 배열의 각 항목은 단건 발송 요청 본문과 완전히
동일하며, idempotencyKey·템플릿·scheduledAt도 항목별로 각각 적용됩니다.
요청
{
"items": [
{
"channel": "sms",
"recipient": { "address": "01012345678" },
"content": { "text": "[Posmit] 점검 안내: 오늘 밤 2시부터 30분간 점검합니다." },
"idempotencyKey": "maintenance-20260710-user1"
},
{
"channel": "sms",
"recipient": { "address": "01098765432" },
"content": { "text": "[Posmit] 점검 안내: 오늘 밤 2시부터 30분간 점검합니다." },
"idempotencyKey": "maintenance-20260710-user2"
}
]
}items가 500개를 초과하거나 항목 중 하나라도 필드 검증에 실패하면 요청 전체가 400으로 거부됩니다.
응답 — 201 Created
{
"data": {
"status": "queued",
"total": 2,
"queued": 1,
"deduplicated": 1,
"items": [
{
"requestId": "9b2f1c3e-8a4d-4c6e-b7f0-1234567890ab",
"status": "queued",
"queueJobId": "1234",
"deduplicated": false,
"scheduledAt": null
},
{
"requestId": "7a1e0d2c-5b3f-4a8e-9c6d-0987654321fe",
"status": "sent",
"queueJobId": null,
"deduplicated": true,
"scheduledAt": null
}
]
},
"meta": { "timestamp": "2026-07-10T09:00:00.000Z" }
}| 필드 | 설명 |
|---|---|
total | 성공적으로 접수된 항목 수 |
queued | 새로 접수된 항목 수 (total - deduplicated) |
deduplicated | idempotencyKey 중복으로 기존 요청이 반환된 항목 수 |
items | 접수된 항목별 결과. 단건 발송 응답과 동일한 구조 |
검증은 통과했지만 접수 과정에서 실패한 항목(예: 템플릿 해석 실패)은 응답 items에서 제외됩니다.
total은 요청한 항목 수가 아니라 성공한 항목 수이므로, total이 보낸 items 개수보다 작으면
일부가 실패한 것입니다. 항목별 idempotencyKey를 지정해 두면 누락분만 안전하게 재요청할 수 있습니다.
상태 조회
notifications:read발송 요청의 현재 상태, 큐 잡 정보, 공급자별 시도 이력을 반환합니다.
requestId는 UUID 형식이어야 합니다. 형식이 다르면 400.- 자신의 워크스페이스 요청만 조회할 수 있습니다. 없으면 404.
curl https://api.posmit.io/v1/notifications/9b2f1c3e-8a4d-4c6e-b7f0-1234567890ab \
-H "X-API-Key: pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRET" \
-H "X-Timestamp: 1752130860" \
-H "X-Nonce: a1b2c3d4e5f60718293a4b5c" \
-H "X-Signature: GENERATED_SIGNATURE"응답 — 200 OK
{
"data": {
"requestId": "9b2f1c3e-8a4d-4c6e-b7f0-1234567890ab",
"workspaceId": "0f8e7d6c-5b4a-3928-1706-fedcba987654",
"channel": "sms",
"status": "sent",
"recipientAddress": "01012345678",
"requestedAt": "2026-07-10T09:00:00.000Z",
"scheduledAt": null,
"selectedProviderAccountId": "3c2b1a09-8f7e-6d5c-4b3a-291807654321",
"selectedProviderMessageId": "M4V20260710090001",
"finalErrorCode": null,
"finalErrorMessage": null,
"job": {
"id": "5d4c3b2a-1908-7f6e-5d4c-3b2a19087f6e",
"queueJobId": "1234",
"status": "completed",
"attemptsMade": 1,
"maxAttempts": 3,
"queuedAt": "2026-07-10T09:00:00.100Z",
"scheduledAt": null,
"startedAt": "2026-07-10T09:00:00.350Z",
"finishedAt": "2026-07-10T09:00:01.200Z",
"lastError": null
},
"attempts": [
{
"notificationJobId": "5d4c3b2a-1908-7f6e-5d4c-3b2a19087f6e",
"providerAccountId": "3c2b1a09-8f7e-6d5c-4b3a-291807654321",
"providerKind": "solapi_sms",
"providerDisplayName": "Solapi SMS",
"status": "success",
"failureCode": null,
"failureMessage": null,
"providerMessageId": "M4V20260710090001",
"startedAt": "2026-07-10T09:00:00.400Z",
"finishedAt": "2026-07-10T09:00:01.150Z"
}
]
},
"meta": { "timestamp": "2026-07-10T09:05:00.000Z" }
}응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
requestId | string (UUID) | 발송 요청 ID |
workspaceId | string (UUID) | 워크스페이스 ID |
channel | string | 발송 채널 |
status | string | queued processing sent failed canceled |
recipientAddress | string | 수신자 주소 |
requestedAt | string | 접수 시각 |
scheduledAt | string 또는 null | 예약 발송 시각 |
selectedProviderAccountId | string 또는 null | 최종 발송에 사용된 공급자 계정 ID |
selectedProviderMessageId | string 또는 null | 공급자가 발급한 메시지 ID |
finalErrorCode | string 또는 null | 최종 실패·취소 사유 코드 (예: canceled_by_api) |
finalErrorMessage | string 또는 null | 최종 실패·취소 사유 메시지 |
job | object 또는 null | 큐 잡 정보 (상태, 시도 횟수 attemptsMade/maxAttempts, 시각, 마지막 에러) |
attempts | array | 공급자별 발송 시도 이력. 시작 시각 오름차순 |
attempts의 각 항목에는 시도한 공급자(providerKind, providerDisplayName), 결과(status),
실패 시 분류 코드(failureCode)가 담깁니다. 폴백이 동작하면 실패한 시도와
성공한 시도가 순서대로 쌓이므로, 어떤 공급자에서 실패해 어디로 대체 발송됐는지 추적할 수 있습니다.
failureCode 값: credit_exhausted(잔액 부족), rate_limited(공급자 한도 초과),
temporary_unavailable(일시 장애), invalid_request, unauthorized, unsupported_channel, unknown
발송 취소
notifications:send아직 발송되지 않은 요청을 취소합니다. 예약 발송 취소에 특히 유용합니다.
curl -X POST https://api.posmit.io/v1/notifications/9b2f1c3e-8a4d-4c6e-b7f0-1234567890ab/cancel \
-H "X-API-Key: pk_xxxxxxxxxxxxxxxxxxxx.YOUR_KEY_SECRET" \
-H "X-Timestamp: 1752130920" \
-H "X-Nonce: 0e1d2c3b4a5968778695a4b3" \
-H "X-Signature: GENERATED_SIGNATURE"동작
- 상태가
queued또는processing이면canceled로 변경하고, 대기 중인 큐 잡을 제거합니다. 조회 응답의finalErrorCode에는canceled_by_api가 기록됩니다. - 이미 종료 상태(
sent·failed·canceled)이면 아무것도 변경하지 않고 현재 상태를 그대로 반환합니다. - 취소가 수행되면
notification.canceled웹훅 이벤트가 발행됩니다.
응답 — 200 OK
{
"data": {
"requestId": "9b2f1c3e-8a4d-4c6e-b7f0-1234567890ab",
"status": "canceled",
"canceled": true
},
"meta": { "timestamp": "2026-07-10T09:02:00.000Z" }
}| 필드 | 설명 |
|---|---|
status | 처리 후 요청 상태 |
canceled | 이번 호출로 실제 취소가 수행됐는지 여부 |
취소는 발송 경쟁(race)에 안전하지 않을 수 있습니다. 취소 호출 시점에 이미 발송 시도가 진행 중이면
(processing) 요청 상태는 canceled가 되지만 진행 중이던 시도는 중단되지 않아, 메시지가 수신자에게
도달할 수 있습니다. 확실한 취소가 필요하면 여유를 둔 scheduledAt 예약 발송과 함께 사용하세요.
이미 종료된 요청에 대한 취소 응답은 "canceled": false입니다.