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

# WebView + 웹훅 연동

> WebView로 부스트 UI 제공, 웹훅으로 결과 수신하는 연동 방식

PlayCamp가 제공하는 WebView UI를 게임에 임베드하여 크리에이터 부스트 기능을 제공합니다. 별도 UI 개발 없이 가장 빠르게 연동할 수 있는 방식입니다.

## 이 방식이 적합한 경우

* 빠르게 크리에이터 부스트 기능을 제공하고 싶을 때
* 별도 UI 개발 없이 연동하고 싶을 때
* PlayCamp가 제공하는 크리에이터 검색/선택 UI를 그대로 사용해도 될 때

## 전체 흐름

```mermaid theme={null}
sequenceDiagram
    participant U as 게임 유저
    participant C as 게임 클라이언트
    participant S as 게임 서버
    participant P as PlayCamp SDK API

    rect rgb(40, 40, 60)
    Note over U,P: 1. WebView 연동 (크리에이터 부스트 선택)
    U->>C: PlayCamp 아이콘 터치
    C->>S: WebView 열기 요청
    S->>P: OTT 발급 (POST /webview/ott)
    P-->>S: OTT 토큰 반환
    S-->>C: OTT 전달
    C->>P: WebView 열기 (/webview/?ott=xxx)
    U->>P: 크리에이터 선택 → 부스트 등록
    end

    rect rgb(40, 60, 40)
    Note over U,P: 2. 웹훅 콜백 (매칭 결과 수신)
    P-->>S: 웹훅: sponsor.created
    Note right of S: userId + creatorKey 저장
    end

    rect rgb(60, 40, 40)
    Note over U,P: 3. 결제 등록 (인게임 결제 발생 시)
    U->>C: 인게임 결제
    C->>S: 결제 완료 알림
    S->>P: 결제 등록 (POST /payments)
    P-->>S: 크리에이터 귀속 확인
    end

    rect rgb(60, 60, 40)
    Note over U,P: 4. 정산
    Note over P: 월말 정산 → 크리에이터별 수익 계산
    end
```

## 게임사 개발 작업

| # | 작업              | 담당       | 설명                       |
| - | --------------- | -------- | ------------------------ |
| 1 | PlayCamp 진입점 배치 | 게임 클라이언트 | 게임 내 아이콘/버튼 배치           |
| 2 | OTT 발급 엔드포인트    | 게임 서버    | 클라이언트 요청 시 OTT 발급 후 전달   |
| 3 | WebView 호출      | 게임 클라이언트 | OTT로 WebView URL 열기      |
| 4 | 웹훅 수신 엔드포인트     | 게임 서버    | 부스트 매칭 결과 수신             |
| 5 | 결제 등록 API 연동    | 게임 서버    | 인게임 결제 발생 시 PlayCamp로 전송 |

***

## Step 1: OTT 발급 (게임 서버)

게임 서버에서 PlayCamp SDK API로 OTT(One-Time Token)를 발급합니다. OTT는 **1회용, 60초 TTL**이므로 유저가 WebView를 열 때마다 새로 발급해야 합니다.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "https://sandbox-sdk-api.playcamp.io/v1/server/webview/ott" \
      -H "Authorization: Bearer ak_server_xxx:secret" \
      -H "Content-Type: application/json" \
      -d '{
        "userId": "게임_유저_고유번호"
      }'
    ```
  </Tab>

  <Tab title="Node SDK">
    ```typescript theme={null}
    const result = await server.webview.createOtt({
      userId: '게임_유저_고유번호',
    });

    // result.ott → OTT 토큰 (클라이언트에 전달)
    ```
  </Tab>

  <Tab title="Go SDK">
    ```go theme={null}
    result, err := server.Webview.CreateOTT(ctx, playcamp.WebviewOttParams{
        UserID: "게임_유저_고유번호",
    })

    // result.OTT → OTT 토큰 (클라이언트에 전달)
    ```
  </Tab>
</Tabs>

**응답**

```json theme={null}
{
  "data": {
    "ott": "a1b2c3d4e5f6...64자hex",
    "expiresIn": 60
  }
}
```

<Warning>
  OTT는 반드시 **게임 서버에서** 발급해야 합니다. 클라이언트에서 직접 발급하면 Server Key가 노출됩니다.
</Warning>

> 상세: [WebView 연동 - OTT 발급 API](/ko/guides/developers/game-integration/webview#ott-발급-api)

## Step 2: WebView 호출 (게임 클라이언트)

게임 서버로부터 받은 OTT로 WebView URL을 열어줍니다.

### 기본 URL

| 환경          | URL                                                     |
| ----------- | ------------------------------------------------------- |
| **Sandbox** | `https://sandbox-sdk-api.playcamp.io/webview/?ott={토큰}` |
| **Live**    | `https://sdk-api.playcamp.io/webview/?ott={토큰}`         |

### 옵션 파라미터

| 파라미터           | 설명                | 예시                     |
| -------------- | ----------------- | ---------------------- |
| `lang`         | 언어 (ko, en)       | `&lang=ko`             |
| `tabs`         | 표시할 탭 제한          | `&tabs=sponsor`        |
| `primaryColor` | 테마 색상 (hex, # 없이) | `&primaryColor=FF6B35` |

**전체 예시**

```
https://sandbox-sdk-api.playcamp.io/webview/?ott=a1b2c3d4...&lang=ko&tabs=sponsor&primaryColor=FF6B35
```

<Info>
  WebView는 게임 내 임베디드 브라우저, 외부 브라우저(모바일/PC) 모두 지원됩니다. 게임 환경에 맞게 선택하세요.
</Info>

> 상세: [WebView 연동 - URL 구성](/ko/guides/developers/game-integration/webview#webview-url-구성)

## Step 3: 웹훅으로 부스트 매칭 수신 (게임 서버)

유저가 WebView에서 크리에이터를 선택하면, PlayCamp가 게임 서버의 웹훅 URL로 매칭 결과를 전송합니다.

### 수신 데이터 (`sponsor.created`)

```json theme={null}
{
  "events": [
    {
      "event": "sponsor.created",
      "timestamp": "2024-01-15T10:30:00.000Z",
      "data": {
        "userId": "게임_유저_고유번호",
        "campaignId": "campaign_001",
        "creatorKey": "ABC12"
      }
    }
  ]
}
```

| 필드           | 설명                          |
| ------------ | --------------------------- |
| `userId`     | 게임 유저 고유번호 (OTT 발급 시 전달한 값) |
| `campaignId` | 캠페인 ID                      |
| `creatorKey` | 유저가 선택한 크리에이터 식별자           |

### 서명 검증

웹훅 헤더의 `X-Webhook-Signature`로 요청이 PlayCamp에서 온 것인지 검증합니다.

<Tabs>
  <Tab title="Node SDK">
    ```typescript theme={null}
    import { verifyWebhook } from '@playcamp/node-sdk';

    app.post('/webhooks/playcamp', express.raw({ type: 'application/json' }), (req, res) => {
      const result = verifyWebhook({
        payload: req.body.toString(),
        signature: req.headers['x-webhook-signature'],
        secret: process.env.WEBHOOK_SECRET,
      });

      if (!result.valid) {
        return res.status(401).json({ error: result.error });
      }

      for (const event of result.payload.events) {
        if (event.event === 'sponsor.created') {
          // userId와 creatorKey 매칭 정보 저장
          console.log(`유저 ${event.data.userId} → 크리에이터 ${event.data.creatorKey}`);
        }
      }

      res.json({ received: true });
    });
    ```
  </Tab>

  <Tab title="Go SDK">
    ```go theme={null}
    func handleWebhook(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)

        result := webhookutil.Verify(webhookutil.VerifyOptions{
            Payload:   body,
            Signature: r.Header.Get("X-Webhook-Signature"),
            Secret:    os.Getenv("WEBHOOK_SECRET"),
        })

        if !result.Valid {
            http.Error(w, result.Error, http.StatusUnauthorized)
            return
        }

        for _, event := range result.Payload.Events {
            if event.Event == playcamp.WebhookEventSponsorCreated {
                // userId와 creatorKey 매칭 정보 저장
                fmt.Printf("유저 %s → 크리에이터 %s\n", event.Data.UserID, event.Data.CreatorKey)
            }
        }

        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]bool{"received": true})
    }
    ```
  </Tab>
</Tabs>

<Note>
  부스트 해제 시에는 `sponsor.ended` 이벤트가 발생합니다. 동일한 데이터 구조(`userId`, `campaignId`, `creatorKey`)로 전달됩니다.
</Note>

> 상세: [웹훅 이벤트](/ko/guides/developers/game-integration/webhook)

## Step 4: 결제 등록 (게임 서버)

인게임 결제가 발생하면 게임 서버에서 PlayCamp로 결제 정보를 전송합니다. 등록된 결제는 유저가 부스트 중인 크리에이터에게 자동 귀속됩니다.

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "https://sandbox-sdk-api.playcamp.io/v1/server/payments" \
      -H "Authorization: Bearer ak_server_xxx:secret" \
      -H "Content-Type: application/json" \
      -d '{
        "userId": "게임_유저_고유번호",
        "transactionId": "txn_abc123",
        "productId": "gem_pack_100",
        "productName": "100 보석 팩",
        "amount": 9900,
        "currency": "KRW",
        "platform": "Android",
        "distributionType": "MOBILE_STORE",
        "purchasedAt": "2024-01-15T10:30:00.000Z"
      }'
    ```
  </Tab>

  <Tab title="Node SDK">
    ```typescript theme={null}
    const payment = await server.payments.create({
      userId: '게임_유저_고유번호',
      transactionId: 'txn_abc123',
      productId: 'gem_pack_100',
      productName: '100 보석 팩',
      amount: 9900,
      currency: 'KRW',
      platform: 'Android',
      distributionType: 'MOBILE_STORE',
      purchasedAt: new Date('2024-01-15T10:30:00Z'),
    });
    ```
  </Tab>

  <Tab title="Go SDK">
    ```go theme={null}
    purchasedAt, _ := time.Parse(time.RFC3339, "2024-01-15T10:30:00Z")

    payment, err := server.Payments.Create(ctx, playcamp.CreatePaymentParams{
        UserID:           "게임_유저_고유번호",
        TransactionID:    "txn_abc123",
        ProductID:        "gem_pack_100",
        ProductName:      playcamp.String("100 보석 팩"),
        Amount:           9900,
        Currency:         "KRW",
        Platform:         playcamp.PaymentPlatformAndroid,
        DistributionType: playcamp.String("MOBILE_STORE"),
        PurchasedAt:      purchasedAt,
    })
    ```
  </Tab>
</Tabs>

<Info>
  결제는 건별로 등록하거나, 최대 1,000건을 한번에 등록(벌크)할 수 있습니다. 환불 시에는 `POST /payments/{transactionId}/refund`를 호출합니다.
</Info>

> 상세: [결제 등록](/ko/guides/developers/game-integration/payment)

## Step 5: 정산

결제 데이터를 기반으로 월 단위 정산이 진행됩니다.

1. **매출 마감** — 매월 말 기준으로 결제 데이터 집계
2. **매출 대사** — PlayCamp에서 제공하는 정산 내역과 게임사 내부 데이터 대사
3. **정산 입금** — 대사 확인 후 정산 금액 입금
4. **크리에이터 정산** — PlayCamp가 크리에이터에게 수익 분배

> 상세: [정산](/ko/guides/developers/settlement)
