웹훅 등록
curl --request POST \
--url https://sajuapi.dev/v1/webhooks \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"url": "<string>",
"events": [
{}
],
"description": "<string>",
"metadata": {}
}
'import requests
url = "https://sajuapi.dev/v1/webhooks"
payload = {
"url": "<string>",
"events": [{}],
"description": "<string>",
"metadata": {}
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({url: '<string>', events: [{}], description: '<string>', metadata: {}})
};
fetch('https://sajuapi.dev/v1/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sajuapi.dev/v1/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => '<string>',
'events' => [
[
]
],
'description' => '<string>',
'metadata' => [
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sajuapi.dev/v1/webhooks"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"events\": [\n {}\n ],\n \"description\": \"<string>\",\n \"metadata\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sajuapi.dev/v1/webhooks")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"events\": [\n {}\n ],\n \"description\": \"<string>\",\n \"metadata\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sajuapi.dev/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"events\": [\n {}\n ],\n \"description\": \"<string>\",\n \"metadata\": {}\n}"
response = http.request(request)
puts response.read_body웹훅
웹훅 등록
POST
/
v1
/
webhooks
웹훅 등록
curl --request POST \
--url https://sajuapi.dev/v1/webhooks \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <api-key>' \
--data '
{
"url": "<string>",
"events": [
{}
],
"description": "<string>",
"metadata": {}
}
'import requests
url = "https://sajuapi.dev/v1/webhooks"
payload = {
"url": "<string>",
"events": [{}],
"description": "<string>",
"metadata": {}
}
headers = {
"X-API-Key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'X-API-Key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({url: '<string>', events: [{}], description: '<string>', metadata: {}})
};
fetch('https://sajuapi.dev/v1/webhooks', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sajuapi.dev/v1/webhooks",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => '<string>',
'events' => [
[
]
],
'description' => '<string>',
'metadata' => [
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sajuapi.dev/v1/webhooks"
payload := strings.NewReader("{\n \"url\": \"<string>\",\n \"events\": [\n {}\n ],\n \"description\": \"<string>\",\n \"metadata\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("X-API-Key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sajuapi.dev/v1/webhooks")
.header("X-API-Key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"<string>\",\n \"events\": [\n {}\n ],\n \"description\": \"<string>\",\n \"metadata\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sajuapi.dev/v1/webhooks")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["X-API-Key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"<string>\",\n \"events\": [\n {}\n ],\n \"description\": \"<string>\",\n \"metadata\": {}\n}"
response = http.request(request)
puts response.read_bodyv1 Enterprise API (Coming Soon)이 엔드포인트는 Enterprise 버전에서 제공될 예정입니다.
현재는 v0 API를 사용하세요.
웹훅 URL은 HTTPS를 사용해야 하며, 공개적으로 접근 가능해야 합니다. 웹훅 요청은 HMAC-SHA256으로 서명됩니다.
Request Body 파라미터
string
required
웹훅 이벤트를 수신할 URL입니다. HTTPS만 지원됩니다. 최대 2048자입니다.
array
required
구독할 이벤트 목록입니다. 최소 1개 이상의 이벤트를 지정해야 합니다.
string
웹훅에 대한 설명입니다. 최대 200자입니다.
object
웹훅에 첨부할 메타데이터입니다. 키-값 쌍 형태입니다.
지원 이벤트
| 이벤트 | 설명 |
|---|---|
profile.created | 새 프로필이 생성됨 |
profile.updated | 프로필이 수정됨 |
profile.deleted | 프로필이 삭제됨 |
fortune.generated | 운세가 생성됨 |
fortune.cached | 캐시된 운세가 반환됨 |
fortune.deleted | 운세가 삭제됨 |
batch.completed | 배치 작업이 완료됨 |
daily.reset | 자정(KST) 일일 리셋 |
unmasked.accessed | 복호화 엔드포인트 접근 |
Response
성공
웹훅 등록에 성공하면 Webhook 객체가 반환됩니다.secret 필드는 생성 시에만 반환됩니다.
실패
| 상태 코드 | 에러 타입 | 설명 |
|---|---|---|
| 400 | validation_error | 요청 데이터가 유효하지 않음 |
| 401 | authentication_error | API 키가 유효하지 않음 |
| 429 | rate_limited | 요청 한도 초과 |
요청 예시
curl -X POST https://api.sajuapi.dev/v1/webhooks \
-H "X-API-Key: bs_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhooks/saju",
"events": ["fortune.generated", "profile.created", "daily.reset"],
"description": "운세 생성 알림",
"metadata": {
"environment": "production",
"team": "backend"
}
}'
const response = await fetch('https://api.sajuapi.dev/v1/webhooks', {
method: 'POST',
headers: {
'X-API-Key': 'bs_live_xxx',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://your-server.com/webhooks/saju',
events: ['fortune.generated', 'profile.created', 'daily.reset'],
description: '운세 생성 알림',
metadata: {
environment: 'production',
team: 'backend'
}
})
});
const webhook = await response.json();
// 중요: secret을 안전하게 저장하세요!
console.log('Webhook Secret:', webhook.secret);
import requests
response = requests.post(
'https://api.sajuapi.dev/v1/webhooks',
headers={'X-API-Key': 'bs_live_xxx'},
json={
'url': 'https://your-server.com/webhooks/saju',
'events': ['fortune.generated', 'profile.created', 'daily.reset'],
'description': '운세 생성 알림',
'metadata': {
'environment': 'production',
'team': 'backend'
}
}
)
webhook = response.json()
# 중요: secret을 안전하게 저장하세요!
print('Webhook Secret:', webhook['secret'])
응답 예시
{
"id": "whk_abc123def456",
"url": "https://your-server.com/webhooks/saju",
"events": ["fortune.generated", "profile.created", "daily.reset"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"description": "운세 생성 알림",
"metadata": {
"environment": "production",
"team": "backend"
},
"active": true,
"failure_count": 0,
"created_at": "2025-01-16T09:00:00Z",
"updated_at": "2025-01-16T09:00:00Z"
}
Webhook 객체
| 필드 | 타입 | 설명 |
|---|---|---|
id | string | 웹훅 ID입니다. whk_ 접두사로 시작합니다. |
url | string | 웹훅 수신 URL입니다. |
events | array | 구독 중인 이벤트 목록입니다. |
secret | string | 서명 검증용 시크릿입니다. 생성 시에만 반환됩니다. |
description | string | 웹훅 설명입니다. |
metadata | object | 메타데이터입니다. |
active | boolean | 활성화 상태입니다. |
failure_count | integer | 연속 실패 횟수입니다. 5회 초과 시 비활성화됩니다. |
last_triggered_at | string | 마지막 트리거 시간입니다. |
created_at | string | 생성 시간입니다. |
updated_at | string | 수정 시간입니다. |
secret은 웹훅 생성 시에만 반환됩니다. 이 값을 안전하게 저장하세요. 분실 시 웹훅을 삭제하고 다시 생성해야 합니다.웹훅 페이로드
웹훅 이벤트가 발생하면 다음 형식의 페이로드가 POST 요청으로 전송됩니다.{
"id": "evt_xyz789",
"type": "fortune.generated",
"created_at": "2025-01-16T09:00:00Z",
"data": {
"fortune_id": "ftn_abc123",
"profile_id": "prf_def456",
"score": 85,
"model": "sonnet",
"cached": false
}
}
서명 검증
모든 웹훅 요청에는X-Bithumb-Signature 헤더가 포함됩니다. 이 서명을 검증하여 요청의 진위를 확인하세요.
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js 예시
app.post('/webhooks/saju', (req, res) => {
const signature = req.headers['x-saju-signature'];
const isValid = verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 이벤트 처리
const { type, data } = req.body;
console.log(`Received ${type}:`, data);
res.status(200).json({ received: true });
});
⌘I