Webhook 등록
bash
curl -X POST https://api.moai.page/v1/webhooks \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-backend.example.com/webhooks/moai",
"events": ["quota.warning", "admesh.creative.approved"],
"secret": "whsec_your_random_string"
}'secret은 클라이언트가 직접 생성한 임의의 강력한 문자열입니다(저장되지 않고 해시 비교에만 사용됨). 분실 시 webhook을 삭제하고 재등록해야 합니다.
이벤트 종류
quota.warning— 모듈 quota 80% 도달quota.exceeded— 모듈 quota 100% 도달apikey.revoked— 키 회수admesh.creative.approved/admesh.creative.rejected— 광고 검수 결과admesh.auction.settled— 매월 말일 경매 낙찰chat.message.created— 새 채팅 메시지 생성anon.post.hidden— 신고 누적 자동 숨김
HMAC 서명 검증
모든 webhook 요청은 X-Moai-Signature 헤더에 sha256=<hex> 형식의 서명을 포함합니다. 본문(raw body)과 등록한 secret으로 HMAC-SHA256을 계산해 일치 여부를 확인하세요.
verify.ts
import { createHmac, timingSafeEqual } from 'crypto';
import express from 'express';
const app = express();
app.post('/webhooks/moai', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.header('X-Moai-Signature') ?? '';
const expected = 'sha256=' + createHmac('sha256', process.env.MOAI_WEBHOOK_SECRET!)
.update(req.body)
.digest('hex');
const a = Buffer.from(signature);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString());
console.log(event.type, event.data);
res.status(200).send('ok');
});재시도 정책
2xx 응답을 5초 안에 받지 못하면 실패로 간주합니다. 실패 시 exponential backoff(1m / 5m / 30m / 2h / 12h)로 최대 5회 재시도하며, 모두 실패하면 webhook이 자동으로 일시 정지되고 OWNER에게 이메일이 발송됩니다.