FastComments.com

웹훅

FastComments를 사용하면 댓글이 시스템에 추가되거나, 업데이트되거나, 제거될 때마다 API 엔드포인트를 호출할 수 있습니다.

우리는 이를 HTTP/HTTPS를 통한 비동기 웹후크로 구현합니다.

웹훅이란 무엇인가 Internal Link

웹훅은 두 시스템 간의 메커니즘 또는 통합으로, "생산자" (FastComments)가 이벤트를 발생시키고 "소비자" (사용자)가 API 호출을 통해 해당 이벤트를 소비합니다.

지원되는 이벤트 및 리소스 Internal Link

FastComments는 Comment 리소스에 대해서만 웹훅을 지원합니다.

우리는 댓글 생성, 삭제 및 업데이트에 대한 웹훅을 지원합니다.

이 각각은 우리 시스템에서 별개의 이벤트로 간주되며, 따라서 서로 다른 의미를 그리고 웹훅 이벤트에 대한 구조를 가집니다.

대시보드 또는 API를 통해 동일한 이벤트에 여러 엔드포인트가 (Managing Webhooks via the API 참조). 각 웹훅은 독립적으로 전달됩니다.

테스트 Internal Link

새 웹훅 및 웹훅 편집 페이지에는 폼에 현재 입력된 URL로 저장 여부와 관계없이 요청을 보내는 Send Test Payload 버튼이 있습니다. Create 및 Update 이벤트는 더미 WebhookComment 객체를 전송하며, Delete 테스트 시에는 ID만 포함된 더미 요청 본문을 전송합니다.

페이로드 검증

웹훅 통합을 테스트할 때, 수신되는 요청에 다음 헤더가 포함되어 있는지 확인하세요:

  1. X-FastComments-Timestamp - Unix 타임스탬프 (초)
  2. X-FastComments-Signature - HMAC-SHA256 서명

서명 스키마가 도입되기 전에 생성된 웹훅은 API Secret을 담고 있는 token 헤더도 함께 수신합니다. 새 웹훅에는 포함되지 않습니다.

페이로드의 진위성을 확인하려면 HMAC 서명 검증을 사용하세요.

테스트 도구

개발 중 수신되는 웹훅 페이로드를 검사하기 위해 webhook.site 또는 ngrok과 같은 도구를 사용할 수 있습니다.

이벤트 유형

  • Create 이벤트: 새 댓글이 생성될 때 트리거됩니다.
  • Update 이벤트: 댓글이 수정될 때 트리거됩니다.
  • Delete 이벤트: 댓글이 삭제될 때 트리거됩니다.

각 웹훅은 하나의 이벤트와 하나의 HTTP 메서드(POST, PUT 또는 DELETE)에 연결됩니다. 각 이벤트는 요청 본문에 전체 댓글 데이터를 포함합니다(페이로드 형식은 데이터 구조 참조).

데이터 구조 Internal Link

웹훅을 통해 전송되는 유일한 구조는 WebhookComment 객체이며, 아래 TypeScript로 정리되어 있습니다.

WebhookComment 객체 구조

"Create" 이벤트 구조

"create" 이벤트의 요청 본문은 WebhookComment 객체입니다.

"Update" 이벤트 구조

"update" 이벤트의 요청 본문은 WebhookComment 객체입니다.

"Delete" 이벤트 구조

"delete" 이벤트의 요청 본문은 WebhookComment 객체입니다.

2023년 11월 14일 변경 사항
이전에는 "delete" 이벤트의 요청 본문에 댓글 id만 포함되었습니다. 이제 삭제 시점의 전체 댓글이 포함됩니다.

모든 키는 본문에 항상 존재합니다. 댓글에 특정 필드의 값이 없으면 본문에는 null(불리언의 경우 false, 리스트의 경우 [])이 담기므로, 전달되는 데이터의 형태는 댓글마다 달라지지 않습니다.

WebhookComment 객체
Copy CopyRun External Link
1
2interface WebhookComment {
3 /** The id of the comment. **/
4 id: string
5 /** The id or URL that identifies the comment thread. Normalized. **/
6 urlId: string
7 /** The URL that points to where the comment was left. **/
8 url: string | null
9 /** The user id that left the comment. If SSO, prefixed with tenant id. **/
10 userId: string | null
11 /** The email of the user left the comment. **/
12 commenterEmail: string | null
13 /** The name of the user that shows in the comment widget. With SSO, can be displayName. **/
14 commenterName: string
15 /** Raw comment text. **/
16 comment: string
17 /** Comment text after parsing. **/
18 commentHTML: string
19 /** Comment external id. **/
20 externalId: string | null
21 /** The id of the parent comment. **/
22 parentId: string | null
23 /** The UTC date when the comment was left. **/
24 date: UTC_ISO_DateString
25 /** Combined karma (up - down) of votes. **/
26 votes: number
27 votesUp: number
28 votesDown: number
29 /** True if the user was logged in when they commented, or their verified the comment, or if they verified their session when the comment was left. **/
30 verified: boolean
31 /** The UTC date when the comment was verified. **/
32 verifiedDate: UTC_ISO_DateString | null
33 /** If a moderator marked the comment reviewed. **/
34 reviewed: boolean
35 /** The location, or base64 encoding, of the avatar. Will only be base64 if that was the value passed with SSO. **/
36 avatarSrc: string | null
37 /** Was the comment manually or automatically marked as spam? **/
38 isSpam: boolean
39 /** Was the comment automatically marked as spam? **/
40 aiDeterminedSpam: boolean
41 /** Are there images in the comment? **/
42 hasImages: boolean
43 /** The page number the comment is on for the "Most Relevant" sort direction. **/
44 pageNumber: number | null
45 /** The page number the comment is on for the "Oldest First" sort direction. **/
46 pageNumberOF: number | null
47 /** The page number the comment is on for the "Newest First" sort direction. **/
48 pageNumberNF: number | null
49 /** Was the comment approved automatically or manually? **/
50 approved: boolean
51 /** The locale code (format: en_us) of the user when the comment was written. **/
52 locale: string | null
53 /** The @mentions written in the comment that were successfully parsed. Empty when there are none. **/
54 mentions: CommentUserMention[]
55 /** The domain the comment is from. **/
56 domain: string | null
57 /** The moderation group ids associated with this comment. Empty when there are none. **/
58 moderationGroupIds: string[]
59 /** The badges shown next to the commenter's name on this comment, as of when the event was created. Empty when there are none. **/
60 badges: CommentUserBadgeInfo[]
61}
62

댓글에서 사용자가 태그되면 해당 정보는 mentions라는 리스트에 저장됩니다. 이 리스트의 각 객체는 다음과 같은 구조를 가집니다.

웹훅 Mentions 객체
Copy CopyRun External Link
1
2interface CommentUserMention {
3 /** The user id. For SSO users, this will have your tenant id prefixed. **/
4 id: string
5 /** The final @mention tag text, including the @ symbol. **/
6 tag: string
7 /** The original @mention tag text, including the @ symbol. **/
8 rawTag: string
9 /** What type of user was tagged. user = FastComments.com account. sso = SSOUser. **/
10 type: 'user'|'sso'
11 /** If the user opts out of notifications, this will still be set to true. **/
12 sent: boolean
13}
14

badges 리스트의 각 항목은 다음과 같은 구조를 가집니다. 배지 유형에 대해서는 배지 가이드를 참조하세요.

웹훅 배지 객체
Copy CopyRun External Link
1
2interface CommentUserBadgeInfo {
3 /** The id of the badge awarded to this user. **/
4 id: string
5 /** The badge type. **/
6 type: number
7 /** The badge description, shown when hovering over the badge. **/
8 description: string
9 /** The text shown on the badge. **/
10 displayLabel?: string | null
11 /** The image shown for the badge, if it uses one. **/
12 displaySrc?: string | null
13 backgroundColor?: string | null
14 borderColor?: string | null
15 textColor?: string | null
16 /** An extra CSS class applied to the badge. **/
17 cssClass?: string | null
18}
19

HTTP 메서드

관리자 패널에서 각 웹훅 이벤트 유형에 대한 HTTP 메서드를 설정할 수 있습니다:

  • Create 이벤트: POST 또는 PUT (기본값: PUT)
  • Update 이벤트: POST 또는 PUT (기본값: PUT)
  • Delete 이벤트: DELETE, POST 또는 PUT (기본값: DELETE)

모든 요청에 ID가 포함되므로, Create 및 Update 작업은 기본적으로(PUT) 멱등성을 가집니다. 동일한 Create 또는 Update 요청을 반복해도 사용자 측에 중복 객체가 생성되지 않아야 합니다.

요청 헤더

각 웹훅 요청에는 다음 헤더가 포함됩니다:

헤더설명
Content-Typeapplication/json
token귀하의 API Secret
X-FastComments-Timestamp요청이 서명된 Unix 타임스탬프(초)
X-FastComments-SignatureHMAC-SHA256 서명 (sha256=<hex>)

HMAC 서명 검증에 대한 정보는 보안 및 API 토큰을 참조하세요.

보안 및 API 토큰 Internal Link

FastComments 웹훅 요청에는 보안을 위해 여러 인증 메커니즘이 포함되어 있습니다.

전송되는 헤더

헤더설명
token귀하의 API Secret(하위 호환성용)
X-FastComments-Timestamp요청이 서명될 때의 Unix 타임스탬프(초)
X-FastComments-Signature페이로드의 HMAC-SHA256 서명

HMAC 서명 검증 (권장)

웹훅 페이로드가 진본이며 변조되지 않았는지 확인하기 위해 HMAC 서명 검증을 강력히 권장합니다.

서명 형식: sha256=<hex-encoded-signature>

서명이 계산되는 방법:

  1. 결합: timestamp + "." + JSON_payload_body
  2. API Secret을 키로 사용하여 HMAC-SHA256을 계산
  3. 결과를 16진수로 인코딩

예시 검증 (Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(req, apiSecret) {
    const timestamp = req.headers['x-fastcomments-timestamp'];
    const signature = req.headers['x-fastcomments-signature'];

    if (!timestamp || !signature) {
        return false;
    }

    // 타임스탬프가 최근인지 확인합니다 (5분 이내)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
        return false;  // 재생 공격 방지
    }

    // 서명 검증
    const payload = JSON.stringify(req.body);
    const expectedSignature = crypto
        .createHmac('sha256', apiSecret)
        .update(`${timestamp}.${payload}`)
        .digest('hex');

    return signature === `sha256=${expectedSignature}`;
}

예시 검증 (Python)

import hmac
import hashlib
import time
import json

def verify_webhook_signature(headers, body, api_secret):
    timestamp = headers.get('X-FastComments-Timestamp')
    signature = headers.get('X-FastComments-Signature')

    if not timestamp or not signature:
        return False

    # 타임스탬프가 최신인지 확인합니다
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False

    # 서명 검증
    payload = json.dumps(body, separators=(',', ':'))
    message = f"{timestamp}.{payload}"
    expected = hmac.new(
        api_secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    return signature == f"sha256={expected}"

예시 검증 (PHP)

function verifyWebhookSignature($headers, $body, $apiSecret) {
    $timestamp = $headers['X-FastComments-Timestamp'] ?? null;
    $signature = $headers['X-FastComments-Signature'] ?? null;

    if (!$timestamp || !$signature) {
        return false;
    }

    // 타임스탬프가 최신인지 확인합니다 (5분 이내)
    $now = time();
    if (abs($now - intval($timestamp)) > 300) {
        return false;
    }

    // 서명 검증
    $payload = json_encode($body, JSON_UNESCAPED_SLASHES);
    $message = $timestamp . '.' . $payload;
    $expectedSignature = 'sha256=' . hash_hmac('sha256', $message, $apiSecret);

    return hash_equals($expectedSignature, $signature);
}

레거시 인증

하위 호환성을 위해 귀하의 API Secret이 포함된 token 헤더는 여전히 전송됩니다. 그러나 HMAC 검증은 재생 공격으로부터 보호하므로 보안 향상을 위해 HMAC 검증으로 마이그레이션할 것을 권장합니다.

API를 통한 웹훅 관리 Internal Link

Webhooks는 REST API를 통해서도 관리할 수 있습니다. 이는 Zapier와 같은 통합이 대시보드에 손대지 않고도 댓글 이벤트를 구독하는 방식이며, REST Hooks 패턴을 따릅니다: 구독, 이벤트 수신, 구독 취소.

API 구독은 대시보드에 설정된 웹훅과 함께 존재합니다. 댓글 이벤트는 해당 도메인과 일치하는 모든 웹훅에 각각 별개의 전달로 전송되며, 웹훅이 생성된 방식에 관계없이 전달됩니다.

인증

모든 요청에는 x-api-key 헤더(또는 API_KEY 쿼리 매개변수)에 API 키를, tenantId 쿼리 매개변수에 테넌트 ID를 포함해야 합니다. 두 값은 대시보드의 API 비밀 페이지에 표시됩니다.

구독

POST https://fastcomments.com/api/v1/webhooks?tenantId=YOUR_TENANT_ID
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
    "url": "https://hooks.zapier.com/hooks/catch/123/abc",
    "event": "comment-created"
}
FieldRequiredDescription
urlYes절대적인 http 또는 https URL입니다.
eventYescomment-created, comment-updated 또는 comment-deleted.
domainNo계정 설정에서 지정한 도메인입니다. 기본값은 *, 이는 모든 도메인의 이벤트를 수신합니다.
methodNoPOST(기본값), PUT 또는 DELETE.

응답에는 구독 정보가 포함됩니다:

{
    "status": "success",
    "webhook": {
        "id": "66f1c4c1e7a2b3d4f5a6b7c8",
        "url": "https://hooks.zapier.com/hooks/catch/123/abc",
        "event": "comment-created",
        "domain": "*",
        "method": "POST",
        "source": "api",
        "enabled": true,
        "createdAt": "2026-09-08T12:00:00.000Z"
    }
}

같은 URL을 동일한 이벤트와 도메인에 다시 구독하면 중복을 생성하는 대신 기존 구독을 반환하므로 클라이언트가 안전하게 재시도할 수 있습니다. 각 테넌트는 최대 50개의 API 구독을 가질 수 있습니다.

목록

GET https://fastcomments.com/api/v1/webhooks?tenantId=YOUR_TENANT_ID

테넌트에 대한 모든 웹훅을 반환하며, 대시보드에서 관리되는 웹훅("source": "dashboard")도 포함됩니다. event, domain 또는 source 로 필터링할 수 있습니다.

구독 취소

DELETE https://fastcomments.com/api/v1/webhooks/SUBSCRIPTION_ID?tenantId=YOUR_TENANT_ID

구독을 삭제하면 해당 구독에 아직 대기 중인 이벤트도 모두 폐기됩니다. 이 방법으로 삭제할 수 있는 것은 API를 통해 생성된 구독뿐이며, 대시보드 웹훅이나 계정에 존재하지 않는 ID는 404와 코드 not-found를 반환합니다. 대시보드 웹훅은 웹훅 페이지에서 편집합니다.

페이로드 및 서명

전달은 대시보드 웹훅과 동일한 페이로드를 사용합니다(데이터 구조 참조) 및 동일한 HMAC 방식으로 서명됩니다(보안 및 API 토큰 참조). API 구독은 레거시 token 헤더를 받지 않으므로 대신 X-FastComments-Signature 헤더를 검증합니다.

샘플 페이로드

GET https://fastcomments.com/api/v1/webhooks/sample-payloads?tenantId=YOUR_TENANT_ID&event=comment-created&limit=3

전달이 포함하는 형태와 정확히 동일하게 계정의 최신 댓글을 반환하므로, 통합에서 첫 이벤트가 도착하기 전에 실제 샘플 데이터를 표시할 수 있습니다. event는 선택 사항이며 검증만 수행됩니다(모든 이벤트가 동일한 댓글 객체를 전달하기 때문). limit은 기본값이 3이며 1에서 10까지 허용됩니다. API 크레딧 2개가 소모됩니다.

{
    "status": "success",
    "payloads": [
        {
            "id": "66f1c4c1e7a2b3d4f5a6b7c8",
            "urlId": "https://example.com/blog/hello-world",
            "commenterName": "Jane Reader",
            "comment": "Great article!",
            "date": "2026-09-08T12:00:00.000Z",
            "approved": true
        }
    ]
}

410 Gone 응답

API 구독의 엔드포인트가 HTTP 410 Gone 응답을 반환하면 FastComments는 이를 구독 취소로 간주합니다: 구독이 삭제되고 대기 중인 이벤트도 함께 삭제되며, 이후 전달이 시도되지 않습니다. 대시보드에 설정된 웹훅은 자동으로 삭제되지 않으며, 이 경우 410은 일반적인 실패로 처리됩니다. 다른 실패 상태는 재시도되며 결국 웹훅이 비활성화됩니다. 자세한 내용은 작동 방식 및 재시도 처리 를 참고하십시오.

대시보드

API 구독은 웹훅 목록에 API 소스로 표시되며, 관리자는 이를 편집, 비활성화, 재활성화 또는 삭제할 수 있습니다.

결론

이로써 우리의 Webhooks 문서가 마무리됩니다.

FastComments Webhook 통합이 이해하기 쉽고 빠르게 설정되기를 바랍니다.

문서에서 누락된 부분을 발견하셨다고 생각되면 아래에 알려주세요.