웹훅 받기

외부 서비스가 보내는 웹훅을 접수창구로 받아, 검증을 거쳐 앱까지 안전하게 전달해요.

결제 완료나 GitHub push처럼 외부 서비스가 보내 주는 소식(웹훅)을 앱이 받을 수 있어요.
외부에는 AxHub의 접수창구 주소만 공개되고, 검증을 통과한 요청만 앱에 전달돼요.

어떻게 동작하나

외부 서비스 → 접수창구 (hooks.{회사}… /r/{창구 id}) → 발신자 검증 → 앱의 수신 경로
  • 접수창구 주소는 앱 주소와 분리돼 있어요. 앱을 다시 배포하거나 내부 경로를 바꿔도 외부에 알려 준 주소는 그대로예요.
  • 검증을 통과한 요청만 앱에 도달하고, 원본 요청의 메서드와 바디는 그대로 보존돼요. 쿼리 스트링은 앱까지 전달되지 않아요.

준비물

  • 연결된 앱 — 웹훅은 앱 단위로 받아요. 아직 없다면 앱 만들고 배포하기부터 하세요.
  • CLI 로그인 — 창구 관리는 axhub relay 명령으로 해요.

접수창구 만들기

axhub relay create --app <앱> --name <창구-이름> --verify-mode key

출력에 받는 주소와 열쇠(키), 수신 계약이 나와요. 이 출력이 기준이에요.

키는 이때 한 번만 보여요. 잃어버리면 axhub relay rotate-key로 새로 발급하세요.

발신자 검증(--verify-mode)은 외부 서비스에 맞춰 세 가지 중 골라요.

verify-mode발신자가 보내는 것
key (기본)X-AxHub-Key 헤더에 공유 키
hmac원문 바디의 HMAC-SHA256을 X-Signature-256: sha256=<hex> 헤더로
none검증 없음. 신뢰할 수 있는 발신자에게만 쓰세요

앱에서 받기

받는 쪽은 SDK가 아니라 프레임워크의 평범한 라우트 핸들러예요. Next.js라면 이렇게 생겼어요.

app/api/hook/route.ts
import { createHmac, timingSafeEqual } from 'crypto';

export async function POST(req: Request): Promise<Response> {
  const secret = process.env.AXHUB_RELAY_SIGNING_SECRET ?? '';
  const rawBody = await req.text();
  const provided = req.headers.get('x-axhub-signature') ?? '';
  const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
  const ok = secret !== '' && provided.length === expected.length &&
    timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
  if (!ok) {
    return new Response('unauthorized', { status: 401 });
  }
  const deliveryId = req.headers.get('x-axhub-delivery') ?? '';
  // 재시도로 같은 전달이 다시 올 수 있어요 — deliveryId 기준으로 한 번만 처리하세요.
  console.log('inbound webhook', deliveryId, rawBody.length);
  // 처리가 끝나면 2xx를 돌려주세요. durable 창구는 2xx가 아니면 다시 전달해요.
  return new Response('ok', { status: 200 });
}
  • 전달마다 X-AxHub-Signature: sha256=<hex> 서명이 붙어요. 배포 때 자동 주입되는 AXHUB_RELAY_SIGNING_SECRET 환경변수로 원문 바디의 HMAC-SHA256을 재계산해 비교하면, 접수창구를 거쳐 온 요청만 받게 돼요. 따로 설정할 건 없어요.
  • 전달마다 X-AxHub-Delivery 헤더(전달 고유 id)도 붙어요. 재시도로 같은 전달이 두 번 올 수 있으니, 이 값으로 한 번만 처리하게 만드세요.
  • 열쇠(키)는 발신자와 접수창구 사이에서만 쓰여요. 접수창구가 검사를 마치면 X-AxHub-Key 같은 자격증명 헤더는 지우고 전달하니, 앱에서 키를 다시 검사하지 마세요.

여러 서비스로 된 앱이라면

docker compose 로 여러 서비스를 함께 올린 앱은 진입 서비스가 웹훅을 받아요. 앱 주소를 열었을 때 응답하는 그 서비스예요.

  • 진입 서비스는 compose 파일에서 호스트 포트를 연 첫 서비스예요. axhub.yaml 의 runtime.entry_service 로 직접 지정할 수도 있어요.
  • 진입 서비스를 바꿔도 접수창구는 그대로 써요. 다시 만들지 않아도 다음 배포부터 새 서비스로 전달돼요.
  • 포트를 연 서비스가 하나도 없으면 받을 곳이 없어서 창구를 만들 수 없어요. 이때는 runtime.entry_service 로 받을 서비스를 지정하면 돼요.

진입 서비스를 고르는 규칙은 axhub.yaml 레퍼런스에서 안내해요.

스테이징으로 받기

연동을 운영에 붙이기 전에 스테이징 배포로 먼저 받아 볼 수 있어요. 창구를 하나 더 만들 필요는 없어요.

스테이징을 쓰는 앱이면 창구 하나에 주소가 둘이에요.

운영      hooks.{회사}.axhub.ai/r/{창구 id}           → 운영 배포
스테이징  hooks--staging.{회사}.axhub.ai/r/{창구 id}   → 스테이징 배포
  • 앱 주소가 {앱}--staging.{회사} 로 갈라지는 것과 같은 규칙이에요. 요청이 들어온 주소가 어느 배포로 갈지 정해요.
  • 열쇠는 주소마다 따로예요. 스테이징 열쇠를 시험용 발신자에게 줘도 운영 주소로는 쓸 수 없어요.
  • axhub relay create 출력과 axhub relay list 에 두 주소가 함께 나와요. 한쪽 열쇠만 갈고 싶으면 axhub relay rotate-key <창구 id> --app <앱> --environment staging --execute 처럼 환경을 골라요.
  • 전달 이력에는 어느 주소로 들어온 요청인지 함께 남아요.

스테이징을 쓰지 않는 앱에는 스테이징 주소가 만들어지지 않아요. 먼저 앱 설정에서 스테이징을 켜 주세요.

전달과 재시도

전달 모드동작
durable (기본)앱이 2xx를 줄 때까지 간격을 늘려 가며 다시 보내요 (1분 → 5분 → 30분 → 2시간 → 6시간, 그 뒤 실패 처리)
sync앱의 응답을 발신자에게 그대로 돌려줘요. 상태 코드와 바디는 물론 Content-Type 같은 응답 헤더도 함께 가요

sync 창구는 앱이 응답을 주면 거기서 끝나요. 발신자가 그 응답을 이미 받았으니 다시 보내지 않고, 다시 시도할지는 발신자가 정해요. 앱에 닿지 못했을 때만 위 간격으로 다시 보내요.

요청 본문은 기본 1MiB까지예요(넘으면 413). 접수는 분당 기본 300건까지고, 넘으면 429가 나요.

운영하기

하고 싶은 일명령
창구 목록 보기axhub relay list --app <앱>
전달 이력 보기 (상태 · 시도 · 응답 코드)axhub relay deliveries <창구 id> --app <앱>
실패한 전달 다시 보내기axhub relay replay <창구 id> <전달 id> --app <앱> --execute
키 교체하기axhub relay rotate-key <창구 id> --app <앱> --execute
스테이징 키만 교체하기axhub relay rotate-key <창구 id> --app <앱> --environment staging --execute
창구 폐쇄하기 (되돌릴 수 없음)axhub relay delete <창구 id> --app <앱> --execute

이렇게 보이면 성공

외부 서비스의 웹훅 설정에 접수창구 주소와 키를 넣고 시험 이벤트를 보내 보세요.
axhub relay deliveries에 전달이 기록되고 응답 코드가 2xx면 성공이에요.