# 포스트백 연동

사용자가 광고 활동(CPA, 미션, 퀴즈 등)을 완료하면 마이비 애드체인 서버에서 파트너 서버로 실시간 콜백을 전송합니다. 파트너사는 포스트백으로 유저에게 포인트를 지급해야합니다.

```mermaid
sequenceDiagram
      participant U as 👤 유저
      participant P as 🏢 연동업체
      participant A as ⚡ 애드체인
      participant AD as 📺 광고상품

      U->>A: ① 광고 참여 클릭
      A->>AD: ② 광고 실행
      U->>AD: ③ 광고 화면으로 이동
      U->>AD: ④ 광고 미션 완료
      AD-->>A: ⑤ 완료 포스트백 전송
      A-->>P: ⑥ 지급 포스트백 전송 (포인트 지급 요청)
      Note over P: ⑦ 포인트 지급
      P-->>U: ⑧ 포인트 지급 알림
```

***

## API 명세

### 요청 정보

| 항목               | 값                     |
| ---------------- | --------------------- |
| **Method**       | `POST`                |
| **Content-Type** | `application/json`    |
| **Endpoint**     | 파트너사가 제공한 포스트백 수신 URL |

### 요청 본문 (Request Body)

```json
{
  "callback_id": "b6fcca4e-e7b8-4a70-94fd-810b1b6a256b",
  "type": "campaign",
  "revenue_type": "cpa",
  "user_id": "ab0da900-7465-4231-8657-1ef40944a8a2",
  "amount": "100",
  "campaign_key": "12352221",
  "campaign_name": "[초간단] 이마트 24 구독하기",
  "signed_value": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "app_key": "100000001",
  "os": "android",
  "ifa": "9ee20401-14bf-4569-a8d3-dc577be8d07f"
}
```

### 필드 설명

| 필드명             | 타입     | 최대 길이 |  필수 | 설명                                                        | 예시                  |
| --------------- | ------ | :---: | :-: | --------------------------------------------------------- | ------------------- |
| `callback_id`   | String |   36  |  ✅  | 고유 트랜잭션 ID (UUID). 중복 지급 방지용                              | `b6fcca4e-e7b8-...` |
| `type`          | String |   32  |  ✅  | 콘텐츠/활동 유형 (`campaign`, `mission`, `quiz`, `...`)          | `campaign`          |
| `revenue_type`  | String |   32  |  ✅  | 수익화 모델 (`cpa`, `cpq`, `cps`, `cpx`, `cpi`, `none`, `...`) | `cpa`               |
| `user_id`       | String |   -   |  ✅  | 파트너사 유저 식별값                                               | `ab0da900-7465-...` |
| `amount`        | String |   20  |  ✅  | 사용자 보상 금액                                                 | `100`               |
| `campaign_key`  | String |  128  |  ✅  | 캠페인 고유 키 (퀴즈의 경우 이벤트 ID)                                  | `12352221`          |
| `signed_value`  | String |   32  |  ✅  | HMAC-MD5 서명값. 데이터 무결성 검증용                                 | `a1b2c3d4e5f6...`   |
| `campaign_name` | String |  256  |     | 캠페인 명칭                                                    | `[초간단] 이마트 24 구독하기` |
| `app_key`       | String |   32  |     | 앱 식별자. 멀티 앱 구분용                                           | `100000001`         |
| `os`            | String |   10  |     | 운영체제 타입 (`android`, `ios`)                                | `android`           |
| `ifa`           | String |  128  |     | 광고 식별값 (Android: GAID, iOS: IDFA)                         | `9ee20401-14bf-...` |

***

## 응답 처리

### 응답 형식 (필수)

모든 응답은 반드시 다음 필드를 포함해야 합니다:

| 필드명       | 타입      |  필수 | 설명                        |
| --------- | ------- | :-: | ------------------------- |
| `success` | Boolean |  ✅  | 처리 성공 여부 (`true`/`false`) |
| `message` | String  |  ✅  | 응답 메시지 (빈 문자열 `""` 가능)    |

### 성공 응답

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
    "success": true,
    "message": "Postback received"
}
```

### 실패 응답

```http
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
    "success": false,
    "message": "Invalid user_id"
}
```

***

## 보안 및 검증

### 서명 검증 (Signature Validation)

데이터 무결성과 보안을 위해 모든 포스트백에는 `signed_value` 필드가 포함됩니다. 파트너사는 이 값을 검증하여 요청이 마이비 애드체인으로부터 온 유효한 요청인지 확인해야 합니다.

#### 서명 생성 방식

1. **서명 대상 문자열 생성**:

   ```
   plainText = callback_id + user_id + amount + campaign_key
   ```
2. **HMAC-MD5 해시 생성**:
   * 알고리즘: HMAC-MD5
   * Secret Key: 마이비 애드체인에서 발급한 `app_secret` (OS 또는 app\_key 별로 구분)
   * 결과: 32자리 16진수 문자열 (소문자)

### Secret Key 관리

파트너사는 `app_key`와 `os` 중에 선택하여 다른 `app_secret`을 사용해야 합니다.

#### App Key 기반 구분 (권장)

```javascript
function getAppSecretByAppKey(payload) {
  const APP_SECRETS = {
    100000001: process.env.ANDROID_APP_SECRET,
    100000002: process.env.IOS_APP_SECRET,
  };

  return APP_SECRETS[payload.app_key];
}
```

#### OS 기반 구분

```javascript
function getAppSecretByOs(payload) {
  const OS_SECRETS = {
    android: process.env.ANDROID_DEFAULT_SECRET,
    ios: process.env.IOS_DEFAULT_SECRET,
  };

  return OS_SECRETS[payload.os];
}
```

### 검증 구현 예제

```javascript
const crypto = require('crypto');

// 환경변수에서 OS별/앱별 Secret 관리
const APP_SECRETS = {
  100000001: process.env.ANDROID_APP_SECRET,
  100000002: process.env.IOS_APP_SECRET,
};

const OS_SECRETS = {
  android: process.env.ANDROID_DEFAULT_SECRET,
  ios: process.env.IOS_DEFAULT_SECRET,
};

function getAppSecret(payload) {
  // app_key가 있는 경우 app별 secret 사용
  if (payload.app_key && APP_SECRETS[payload.app_key]) {
    return APP_SECRETS[payload.app_key];
  }
  // OS별 secret 사용
  return OS_SECRETS[payload.os] || OS_SECRETS['android'];
}

function validateSignature(payload) {
  const { callback_id, user_id, amount, campaign_key, signed_value } = payload;

  // 적절한 app_secret 선택
  const appSecret = getAppSecret(payload);

  // 서명 대상 문자열 생성
  const plainText = `${callback_id}${user_id}${amount}${campaign_key}`;

  // HMAC-MD5 해시 생성
  const hmac = crypto.createHmac('md5', appSecret);
  hmac.update(plainText);
  const generatedValue = hmac.digest('hex');

  // 서명 검증
  return generatedValue === signed_value;
}

// 사용 예시
const payload = {
  callback_id: 'b6fcca4e-e7b8-4a70-94fd-810b1b6a256b',
  user_id: 'ab0da900-7465-4231-8657-1ef40944a8a2',
  amount: '100',
  campaign_key: '12352221',
  signed_value: '5d8b2a9f1c3e7b4a6d9f2e8c1a5b3d7f',
  os: 'android',
  app_key: '100000001',
};

if (validateSignature(payload)) {
  console.log('✅ 서명 검증 성공 - 유효한 요청입니다.');
} else {
  console.log('❌ 서명 검증 실패 - 요청을 거부해야 합니다.');
}
```

### 주의사항

1. **app\_secret 보안**: app\_secret은 절대 외부에 노출되어서는 안 됩니다
2. **검증 필수**: 모든 포스트백 요청에 대해 서명 검증을 반드시 수행해야 합니다
3. **검증 실패 시**: 서명 검증이 실패한 요청은 반드시 거부해야 합니다 (HTTP 401 응답)
4. **문자열 순서**: 서명 생성 시 필드 순서가 중요합니다 (`callback_id` → `user_id` → `amount` → `campaign_key`)
5. **Secret 관리**: OS별 또는 앱별로 다른 secret을 사용하므로 환경변수 등을 통해 안전하게 관리해야 합니다
6. **우선순위**: app\_key가 제공된 경우 해당 앱의 secret을 우선 사용하고, 없으면 OS별 기본 secret을 사용합니다

***

## 구현 가이드

### 중복 처리 방지 (필수)

사용자 보상이 직접적으로 연결되어 있기 때문에, 중복 처리 방지 로직을 반드시 구현해야 합니다.

#### 핵심 구현 요구사항

1. **고유 식별자 관리**
   * `callback_id`는 각 트랜잭션의 고유 식별자(UUID)입니다
   * 동일한 `callback_id`가 여러 번 수신될 경우, **반드시 첫 번째 요청만 처리**하고 이후 요청은 무시해야 합니다
2. **구현 예시**

```javascript
async function processPostback(payload) {
  const { callback_id, user_id, amount } = payload;

  // 1. 중복 체크 (필수)
  const isDuplicate = await checkDuplicateCallback(callback_id);
  if (isDuplicate) {
    console.log(`[중복 요청] callback_id: ${callback_id} - 처리 스킵`);
    return { success: true, message: 'Already processed' };
  }

  // 2. callback_id 저장
  await saveCallbackId(callback_id);

  // 3. 실제 포인트 지급 처리
  await grantPointsToUser(user_id, amount);

  return { success: true, message: 'Postback processed' };
}
```

### 푸시 알림 구성 가이드

사용자에게 리워드 지급 푸시 알림을 보낼 때는 `campaign_name`과 `amount` 필드를 활용하여 메시지를 구성하는 것을 권장합니다.

#### 푸시 메시지 예시

```
// 일반 캠페인
"[초간단] 이마트 24 구독하기 참여 완료! 100포인트가 지급되었습니다."

// 미션 완료
"3회 미션 완료 보상으로 500포인트가 지급되었습니다."

// 퀴즈 참여
"일일 상식 퀴즈 참여 보상 50포인트가 지급되었습니다."
```

#### 구현 예시

```javascript
function createPushMessage(payload) {
  const { campaign_name, amount } = payload;

  // campaign_name이 있는 경우
  if (campaign_name) {
    return `${campaign_name} 참여 완료! ${amount}포인트가 지급되었습니다.`;
  }

  // campaign_name이 없는 경우 type 기반 메시지
  switch (payload.type) {
    case 'mission':
      return `미션 완료 보상 ${amount}포인트가 지급되었습니다.`;
    case 'quiz':
      return `퀴즈 참여 보상 ${amount}포인트가 지급되었습니다.`;
    default:
      return `${amount}포인트가 지급되었습니다.`;
  }
}
```

***

## 데이터 타입별 예시

### 1. CPA 광고 캠페인 (일반 참여형)

```json
{
  "callback_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "campaign",
  "revenue_type": "cpa",
  "user_id": "ab0da900-7465-4231-8657-1ef40944a8a2",
  "os": "android",
  "ifa": "abc123def456",
  "amount": "150",
  "campaign_key": "camp_001",
  "campaign_name": "[초간단] 이마트 24 구독하기",
  "signed_value": "7f8a9b2c3d4e5f6a1b2c3d4e5f6a7b8c",
  "app_key": "100000001"
}
```

### 2. 미션 완료 보상

```json
{
  "callback_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
  "type": "mission",
  "revenue_type": "none",
  "user_id": "ab0da900-7465-4231-8657-1ef40944a8a2",
  "os": "ios",
  "ifa": "xyz789abc123",
  "amount": "500",
  "campaign_key": "mission_daily",
  "campaign_name": "3회 미션 완료 보상",
  "signed_value": "9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a",
  "app_key": "100000002"
}
```

### 3. 퀴즈 참여 보상

```json
{
  "callback_id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
  "type": "quiz",
  "revenue_type": "none",
  "user_id": "user_345678",
  "os": "android",
  "ifa": "def456ghi789",
  "amount": "50",
  "event_id": "quiz_2024_01",
  "campaign_key": "quiz_2024_01",
  "campaign_name": "일일 상식 퀴즈",
  "signed_value": "2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e",
  "app_key": "100000002"
}
```

***

## 테스트 가이드

### 테스트 환경 구성

**실제 서비스 적용 전 마이비 애드체인 개발팀과 함께 테스트를 진행해 주세요.**

테스트 포스트백을 받기 위한 Endpoint를 알려주시면 테스트 진행을 도와드리겠습니다.

### 테스트 시나리오

1. **기본 포스트백 수신 테스트**: 정상적인 포스트백 처리 확인
2. **중복 처리 테스트**: 동일한 `callback_id`로 반복 테스트가 필요하신 경우, 개발팀에 요청해 주시면 중복 포스트백을 보내드립니다
3. **서명 검증 테스트**: 올바른 서명과 잘못된 서명 처리 확인
4. **에러 처리 테스트**: 잘못된 데이터 형식 처리 확인

***

## 보안 고려사항

1. **HTTPS 필수**: 모든 포스트백 전송은 HTTPS 프로토콜을 사용합니다
2. **서명 검증 필수**: 모든 포스트백 요청에 대해 `signed_value` 검증을 반드시 수행해야 합니다
3. **Secret Key 관리**: OS별/앱별 `app_secret`을 환경변수나 보안 저장소에 안전하게 분리 보관해야 합니다

***

## 문의사항

연동 관련 문의사항이 있으시면 아래 채널로 연락 주시기 바랍니다:

* 기술 지원: <contact@1self.world>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://adchain-doc.1self.world/s2s/postback.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
