이벤트 종류
| 이벤트 | 설명 |
|---|---|
coupon.redeemed | 쿠폰 사용됨 |
payment.created | 결제 등록됨 |
sponsor.created | 후원 등록됨 |
웹훅 형식
헤더복사
Content-Type: application/json
X-Webhook-Batch: true
X-Webhook-Signature: {HMAC-SHA256 서명}
복사
{
"events": [
{
"event": "payment.created",
"timestamp": "2024-01-15T10:30:00.000Z",
"data": {
"transactionId": "txn_abc123",
"userId": "user_12345",
"amount": 9900,
"currency": "KRW",
"creatorKey": "ABC12"
}
}
]
}
서명 검증
웹훅 요청이 PlayCamp에서 온 것인지 확인하기 위해 서명을 검증해야 합니다.- Node SDK
- Go SDK
- 수동 (crypto)
SDK의
verifyWebhook() 유틸리티로 서명 검증, 타임스탬프 검증, 페이로드 파싱을 처리합니다:복사
import { verifyWebhook } from '@playcamp/node-sdk';
app.post('/webhooks/playcamp', (req, res) => {
const result = verifyWebhook({
payload: req.rawBody,
signature: req.headers['x-webhook-signature'],
secret: process.env.WEBHOOK_SECRET,
tolerance: 300, // 최대 허용 시간 초 (기본값: 300)
});
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
for (const event of result.payload.events) {
switch (event.event) {
case 'coupon.redeemed':
console.log('쿠폰 사용:', event.data.couponCode);
break;
case 'payment.created':
console.log('결제 등록:', event.data.transactionId);
break;
case 'sponsor.created':
console.log('후원 등록:', event.data.userId);
break;
}
}
res.status(200).json({ received: true });
});
SDK의
webhookutil 패키지로 서명 검증, 타임스탬프 검증, 페이로드 파싱을 처리합니다:복사
import "github.com/playcamp/playcamp-go-sdk/webhookutil"
func handleWebhook(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
result := webhookutil.Verify(webhookutil.VerifyOptions{
Payload: body,
Signature: r.Header.Get("X-Webhook-Signature"),
Secret: os.Getenv("WEBHOOK_SECRET"),
Tolerance: 300, // 최대 허용 시간 초 (기본값: 300)
})
if !result.Valid {
http.Error(w, result.Error, http.StatusUnauthorized)
return
}
for _, event := range result.Payload.Events {
switch event.Event {
case playcamp.WebhookEventCouponRedeemed:
fmt.Println("쿠폰 사용")
case playcamp.WebhookEventPaymentCreated:
fmt.Println("결제 등록")
case playcamp.WebhookEventSponsorCreated:
fmt.Println("후원 등록")
}
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"received": true})
}
복사
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string, // raw request body
signature: string, // X-Webhook-Signature 헤더 값
secret: string // 웹훅 시크릿
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
수신 예시 (Express)
- Node SDK
- 수동 (crypto)
복사
import { verifyWebhook } from '@playcamp/node-sdk';
app.post('/webhooks/playcamp', express.raw({ type: 'application/json' }), (req, res) => {
const payload = Buffer.isBuffer(req.body) ? req.body.toString() : req.body;
const result = verifyWebhook({
payload,
signature: req.headers['x-webhook-signature'] as string,
secret: process.env.WEBHOOK_SECRET!,
});
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
for (const event of result.payload.events) {
switch (event.event) {
case 'coupon.redeemed':
console.log('쿠폰 사용:', event.data);
break;
case 'payment.created':
console.log('결제 등록:', event.data);
break;
}
}
res.json({ received: true });
});
복사
import crypto from 'crypto';
app.post('/webhooks/playcamp', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const isBatch = req.headers['x-webhook-batch'] === 'true';
const webhookSecret = process.env.WEBHOOK_SECRET;
const payload = Buffer.isBuffer(req.body)
? req.body.toString()
: req.body;
// 1. 서명 검증
if (webhookSecret && signature) {
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
// 2. JSON 파싱
const data = JSON.parse(payload);
// 3. 이벤트 처리
const events = isBatch || data.events ? data.events : [data];
for (const event of events) {
switch (event.event) {
case 'coupon.redeemed':
console.log('쿠폰 사용:', event.data);
break;
case 'payment.created':
console.log('결제 등록:', event.data);
break;
}
}
res.json({ received: true });
});
express.json() 대신 express.raw({ type: 'application/json' })를 사용해야 원본 body로 서명 검증이 가능합니다.웹훅 테스트
로컬 개발을 위해 테스트 서명을 생성할 수 있습니다:복사
import { constructWebhookSignature } from '@playcamp/node-sdk';
const payload = JSON.stringify({
events: [{
event: 'coupon.redeemed',
timestamp: new Date().toISOString(),
data: { couponCode: 'TEST', userId: 'user_123', usageId: 1, reward: [] },
}]
});
const signature = constructWebhookSignature(payload, 'your_webhook_secret');
웹훅 관리
- cURL
- Node SDK
- Go SDK
| 기능 | Method | Endpoint |
|---|---|---|
| 웹훅 목록 | GET | /v1/server/webhooks |
| 웹훅 등록 | POST | /v1/server/webhooks |
| 웹훅 수정 | PUT | /v1/server/webhooks/:id |
| 웹훅 삭제 | DELETE | /v1/server/webhooks/:id |
| 웹훅 로그 | GET | /v1/server/webhooks/:id/logs |
| 웹훅 테스트 | POST | /v1/server/webhooks/:id/test |
복사
// 웹훅 등록
const webhook = await server.webhooks.create({
eventType: 'coupon.redeemed',
url: 'https://your-server.com/webhooks/playcamp',
});
// 웹훅 목록
const webhooks = await server.webhooks.listWebhooks();
// 웹훅 수정
await server.webhooks.update(webhook.id, {
url: 'https://your-server.com/webhooks/v2',
});
// 웹훅 삭제
await server.webhooks.remove(webhook.id);
// 웹훅 전송 로그
const logs = await server.webhooks.getLogs(webhook.id);
// 테스트 이벤트 전송
await server.webhooks.test(webhook.id);
복사
// 웹훅 등록
webhook, err := server.Webhooks.Create(ctx, playcamp.CreateWebhookParams{
EventType: playcamp.WebhookEventCouponRedeemed,
URL: "https://your-server.com/webhooks/playcamp",
})
// 웹훅 목록
webhooks, err := server.Webhooks.List(ctx)
// 웹훅 수정
updated, err := server.Webhooks.Update(ctx, webhook.ID, playcamp.UpdateWebhookParams{
URL: playcamp.String("https://your-server.com/webhooks/v2"),
})
// 웹훅 삭제
err := server.Webhooks.Delete(ctx, webhook.ID)
// 웹훅 전송 로그
logs, err := server.Webhooks.GetLogs(ctx, webhook.ID)
// 테스트 이벤트 전송
result, err := server.Webhooks.Test(ctx, webhook.ID)