한국어

Webhooks

MOAI Cloud는 quota 경고, 광고 검수 완료, 경험치 시즌 종료 등 주요 이벤트를 webhook으로 발송합니다. 모든 요청은 HMAC-SHA256으로 서명됩니다.

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에게 이메일이 발송됩니다.