> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alforse.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 웹훅

> 구독한 테이블의 레코드가 삽입, 업데이트 또는 삭제될 때마다 Alforse가 서명된 JSON 알림을 사용자의 엔드포인트로 POST합니다.

**웹훅**(webhook)은 구독한 테이블의 레코드가 삽입, 업데이트 또는 삭제될 때마다 Alforse가 지정한 URL로 HTTP `POST`를 보내도록 합니다. 개발자는 API를 폴링하지 않고도 Alforse의 변경 사항에 반응하기 위해 웹훅을 사용합니다.

## 웹훅의 동작 방식

* 각 웹훅은 **하나의 테이블**과 하나 이상의 **트리거 작업**을 감시합니다.
* 일치하는 변경이 발생하면 Alforse는 전송을 큐에 넣고 비동기로 보내며, 보통 몇 초 이내에 전달됩니다.
* 각 전송은 해당 웹훅에 속한 비밀 값으로 서명되므로, 엔드포인트는 요청이 Alforse에서 왔음을 증명할 수 있습니다.
* 본문은 레코드가 변경되었다는 알림입니다. 조치를 취하기 전에 [API 키](https://docs.alforse.com/ko/docs/building/api-keys.md)로 현재 레코드를 가져오십시오.

## 구독 가능한 테이블

| 모듈 | 테이블 |
| - | - |
| 영업 | `salesOrder`, `repairOrder`, `repairOrderLine`, `repairOrderCharge`, `warrantyTerm`, `customerWarrantyTerm`, `warrantyRegistration` |
| 구매 | `purchaseOrder` |
| 재고 | `receipt`, `shipment` |
| 생산 | `job` |
| 품목 | `part`, `material`, `tool`, `consumable`, `service` |
| 품질 | `nonConformance` |
| 재무 | `fixedAsset` |
| 사용자 | `employee`(업데이트만 해당) |

배포 환경에서 허용되는 테이블은 웹훅 양식의 **테이블** 드롭다운을 기준으로 하십시오. 테이블은 일부 작업만 발행하므로 트리거 선택지도 테이블에 따라 달라집니다. `receipt`는 삽입과 삭제를 발행하고, `employee`는 업데이트만 발행합니다.

## 웹훅 설정

웹훅을 생성하고 관리하려면 **설정 → 웹훅**으로 이동합니다. 다음 필드를 입력합니다.

| 필드 | 역할 |
| - | - |
| **이름** | 웹훅의 라벨입니다. 회사 내에서 고유해야 합니다. |
| **테이블** | 감시할 단일 테이블입니다. |
| **URL** | Alforse가 `POST`를 보내는 위치입니다. 공용 주소로 확인되는 `http` 또는 `https` URL이어야 합니다. |
| **트리거** | **삽입**, **업데이트**, **삭제**의 임의 조합입니다. 하나 이상 선택하십시오. |
| **활성** | 기본값은 켜짐입니다. 웹훅을 삭제하지 않고 전송을 일시 중지하려면 끄십시오. |

Alforse는 루프백, 사설 네트워크, 링크 로컬, 클라우드 메타데이터 주소로의 전송을 거부합니다. 이는 웹훅이 내부 시스템에 접근하는 데 악용되는 것을 막습니다.

웹훅 목록에는 전송 시도를 기준으로 집계한 성공 횟수와 오류 횟수가 웹훅별로 표시됩니다.

## 전송 내용

### 본문

본문은 네 개의 필드로 구성된 JSON입니다.

| 필드 | 의미 |
| - | - |
| `type` | `INSERT`, `UPDATE` 또는 `DELETE` |
| `table` | 변경된 테이블 |
| `companyId` | 레코드가 속한 회사 |
| `record` | 영향을 받은 레코드 |

```json theme={null}
{
  "type": "UPDATE",
  "table": "salesOrder",
  "companyId": "abc123",
  "record": { "id": "...", "salesOrderId": "SO000042", "status": "To Ship and Invoice" }
}
```

### 헤더

| 헤더 | 값 |
| - | - |
| `Content-Type` | `application/json` |
| `x-omeriva-webhook-signature` | `v1=<hex>` 형식의 서명([전송 검증](#verifying-deliveries) 참조) |
| `x-omeriva-webhook-timestamp` | 이번 시도의 시각(Unix 초) |
| `x-omeriva-webhook-id` | 웹훅의 id |
| `x-omeriva-webhook-event` | 이벤트 id |

이벤트 id를 멱등성 키로 사용하십시오. 하나의 변경은 모든 재시도에서 같은 이벤트 id를 유지하며, 새로운 변경은 이미 알림을 받은 레코드에 대한 두 번째 업데이트를 포함하여 항상 새로운 id를 받습니다.

## 전송과 재시도

전송은 **최소 한 번**(at least once) 보장됩니다. 엔드포인트가 다음에 해당하면 Alforse는 해당 시도를 실패로 간주하고 지수 백오프로 최대 8회까지 재시도합니다.

* 10초 이내에 성공 상태를 반환하지 않는 경우
* `5xx` 또는 `429`를 반환하는 경우

그 밖의 결과는 재시도하지 않습니다.

* 그 외의 `4xx` 응답은 해당 변경의 전송을 종료합니다.
* Alforse는 리디렉션을 따라가지 않으므로 `3xx` 응답은 실패로 간주됩니다. 최종 URL을 설정하십시오.
* 거부된 주소로의 전송은 재시도하지 않습니다.

마지막 시도까지 실패하면 Alforse는 해당 변경을 실패로 표시하고 폐기합니다.

### 엔드포인트 설계

* **중복을 예상하십시오.** 첫 번째 시도를 이미 처리한 뒤에 재시도가 도착할 수 있습니다. 하나의 레코드에 대한 별개의 변경은 `type`과 `record.id`가 같으므로, 이 조합이 아니라 `x-omeriva-webhook-event`로 중복을 제거하십시오.
* **장애 후에는 정합성을 맞추십시오.** 폐기된 변경은 다시 전송되지 않습니다. 모든 이벤트를 처리해야 한다면 엔드포인트가 중단되었던 이후에 API를 조회하여 따라잡으십시오.

## 전송 검증

웹훅을 생성하면 해당 웹훅 고유의 서명 비밀 값도 생성되며, Alforse는 이를 암호화하여 보관합니다. `x-omeriva-webhook-signature`의 서명은 `` `${timestamp}.${body}` ``에 대한 HMAC-SHA256의 16진수 값이며, 비밀 문자열을 키로 사용합니다. `timestamp`는 `x-omeriva-webhook-timestamp`의 값이고 `body`는 원본 요청 본문입니다.

전송을 검증하려면 다음과 같이 합니다.

1. 파싱하기 전에 원본 본문을 확보합니다. JSON을 파싱했다가 다시 직렬화하면 바이트가 바뀌어 서명이 깨집니다.
2. HMAC을 계산하여 `v1=` 뒤의 값과 상수 시간 비교로 대조합니다.
3. 시계와 5분 넘게 차이 나는 타임스탬프는 거부하여 캡처된 요청의 재생을 막습니다. 모든 재시도는 새 타임스탬프로 다시 서명되므로 정상적인 재시도는 통과합니다.

```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyAlforseSignature(
  rawBody: string,
  signatureHeader: string,
  timestampHeader: string,
  secret: string,
  toleranceSeconds = 300
) {
  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  const received = signatureHeader.replace(/^v1=/, "");
  return (
    received.length === expected.length &&
    timingSafeEqual(Buffer.from(received), Buffer.from(expected))
  );
}
```

NOTE

**서명 비밀 값 받기**

앱은 현재 서명 비밀 값을 표시하거나 교체하는 기능을 제공하지 않습니다. 검증을 설정할 때 Alforse 지원팀에 요청하십시오.

## 문제 해결

| 증상 | 원인 | 해결 방법 |
| - | - | - |
| 서버에 아무것도 도착하지 않음 | URL이 루프백, 사설, 링크 로컬 또는 메타데이터 주소로 확인되어 Alforse가 이를 거부하고 재시도하지 않습니다 | 공개적으로 접근 가능한 URL을 사용하십시오. |
| 서버가 동작 중인데도 모든 시도가 실패함 | URL이 `3xx` 리디렉션을 반환합니다 | 리디렉션이 가리키는 최종 URL을 입력하십시오. |
| 같은 변경이 두 번 처리됨 | 느리거나 실패한 응답 뒤에 재시도가 도착했습니다 | `x-omeriva-webhook-event`로 중복을 제거하십시오. |
| 유효한 요청에서 서명 검사가 실패함 | 해시하기 전에 본문을 파싱했습니다 | 원본 본문 바이트를 해시하십시오. |
| **테이블**에 특정 테이블이 없음 | 해당 테이블은 웹훅 이벤트를 발행하지 않습니다 | 관련 테이블을 구독하거나 API를 폴링하십시오. |

## 관련 문서

**API 키**

웹훅이 변경을 알린 뒤 Alforse API에서 전체 레코드를 가져옵니다.

[Open](https://docs.alforse.com/ko/docs/building/api-keys.md)

**연동**

패키지형 커넥터를 선호하십니까? 자사 연동을 확인하십시오.

[Open](https://docs.alforse.com/ko/docs/integrations.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.