개발 가이드

매체사 연동

본 문서는 Blomics 가 제공하는 게이미피케이션 컨텐츠를 매체사 앱에 연동하기 위한 기술 가이드입니다. 매체사는 이 문서를 참고하여 인증 API, 포스트백 API, 웹뷰 브릿지를 구현해주시기 바랍니다.

연동 전 준비사항

Blomics 연동을 시작하기 전에 다음 정보를 Blomics 담당자로부터 전달받아야 합니다.

  • publisher_id — 매체사 식별값 (UUID)
  • publisher_secret — 매체사 API 인증 비밀키. 32-byte base64url(43자). 등록 시 1회 자동 발급
  • content_id[] — 연동할 콘텐츠 식별값(UUID) 목록. Blomics 운영팀에 요청
  • hmac_secret — 포스트백 서명 검증용 비밀키
  • auth_typeplain(직접 전달) 또는 token(토큰 교환)

매체사가 준비해야 할 것

  • 게이트웨이 URL 연동 — 블로믹스 컨텐트 페이지로 리다이렉트 (필수)
  • 인증 API (auth_url) — token 인증 방식 사용 시 (선택)
  • 포스트백 수신 API (postback_url) — Blomics 가 포인트 적립을 요청 (필수)
  • 웹뷰 브릿지 (BlomicsBridge) — 매체사 앱의 웹뷰에 JavaScript 브릿지 객체를 주입 (권장)
Bearer 정적 키 사용 권장
외부 API 호출 시 Authorization: Basic base64(publisher_id:publisher_secret) 또는Authorization: Bearer <publisher_secret> 둘 다 지원합니다. 신규 통합은 Bearer 권장 — 자세한 내용은 API Reference.

게이트웨이 URL 연동

매체사 앱의 웹뷰 또는 네이티브 화면에서 아래 URL 을 호출하면 Blomics 서버가 사용자를 인증하고 콘텐츠 페이지로 자동 리다이렉트합니다.

http
GET https://added.blomics.net/gateway?publisher_id={uuid}&content_id={uuid}&user_id={string}&ad_id={string}&callback_data={json}
  • publisher_id (UUID, 필수) — 매체사 식별값
  • content_id (UUID, 필수) — 콘텐츠 식별값. 동일 캠페인에 여러 콘텐츠가 있을 수 있음
  • user_id (string, 필수) — 매체 등록 시 선택한 유형에 따라 plain: 사용자 ID 직접 전달 / token: 인증 토큰 전달
  • ad_id (string, 필수) — 광고 식별자 (ADID/IDFA). 값을 가져오지 못한 경우 기본값00000000-0000-0000-0000-000000000000
  • callback_data (json, 선택) — postback API 요청을 받을 때 전달받을 데이터. JSON 형식을 문자열로 전달

인증 방식

게이트웨이 URL 의 user_id 파라미터는 인증 방식에 따라 다르게 사용됩니다.

plain 방식 (직접 전달)
  • 매체사 앱에서 웹뷰를 열 때, 매체사 내부 사용자 고유 ID 를 user_id 값으로 전달합니다.
  • Blomics 서버는 이 값을 그대로 publisher_user_id 로 사용합니다.
  • 별도의 인증 API 구현이 필요 없어 연동이 간단합니다.
token 방식 (토큰 교환)
  • 매체사 앱에서 웹뷰를 열 때, 일회성 인증 토큰을 user_id 값으로 전달합니다.
  • Blomics 서버가 사전에 등록한 매체사의 인증 API(auth_url)를 호출하여 토큰을 검증하고 실제 publisher_user_id 를 획득합니다.
  • 사용자 ID 가 URL 에 노출되지 않아 보안성이 높습니다.

인증 API 구현 (token 방식인 경우만)

token 인증 방식 사용 시, 매체사는 토큰을 검증하고 사용자 정보를 반환하는 API 를 구현해야 합니다.

요청 (Blomics → 매체사)

http
POST {auth_url}
Content-Type: application/json

{ "token": "<일회성 토큰>" }

응답 (매체사 → Blomics)

HttpStatusCode 2xx 을 성공으로 처리하고 그 외에는 모두 실패로 처리합니다. 실패 시 1회 재시도 합니다.

json
{ "user_id": "매체측 사용자 ID" }

포스트백 API 구현 (필수)

사용자가 리워드 조건을 달성하면 Blomics 서버가 매체사의 포스트백 API 를 호출합니다. 사용자가 리워드 조건을 달성하지 못한 이벤트에 대해서도 포스트백을 받기 위해서는 매체사 속성의 send_postback_on_reject 를 활성화해야 합니다.

요청 (Blomics → 매체사)

  • postback_id — 고유 식별값. 재시도 시 동일 ID 가 올 수 있어 중복 적립을 방지해야 함
  • user_id — 게이트웨이 URL 로 전달한 매체사 사용자 ID
  • content_id — 콘텐츠 식별값
  • campaign_id — 캠페인 식별값
  • campaign_name — 캠페인 이름
  • event_id — 이벤트 식별값
  • event_type — 이벤트 유형
  • event_name — 이벤트 이름
  • campaign_typealways_on 또는 promotion
  • parent_campaign_id — promotion 의 경우 부모 캠페인 UUID
  • point — 지급할 포인트
  • total_point — 캠페인 동안 지급할 전체 포인트
  • created_at — 포스트백 요청 시간
  • hmac — HMAC 서명값
  • callback_data — 게이트웨이 URL 로 전달한 값
http
POST {postback_url}
Content-Type: application/json

{
  "postback_id": "01H...",
  "user_id": "user_12345",
  "content_id": "0192...",
  "campaign_id": "0193...",
  "campaign_name": "두더지잡기 광고 리워드",
  "event_id": "0194...",
  "event_type": "complete",
  "event_name": "한 판 완료",
  "campaign_type": "always_on",
  "parent_campaign_id": null,
  "point": 100,
  "total_point": 500000,
  "created_at": "2026-05-02T10:00:00Z",
  "hmac": "<sha256 hex>",
  "callback_data": "{\"req_id\":\"...\"}"
}

응답 처리

매체는 Blomics 가 보낸 포스트백에 대해 HTTP 상태와 JSON body 로 응답합니다. HTTP 상태가 재시도 여부를 결정합니다.

  • 200 / 204 — 정상 처리 또는 동일 postback_id 중복 수신의 멱등 처리. Blomics 는 재시도하지 않고 postbacks.statussuccess 로 마킹합니다.
  • 400 INVALID_PARAM — payload 형식 오류. exponential backoff 재시도(최대 5회).
  • 401 HMAC_MISMATCH — HMAC 검증 실패. 시크릿 동기화 필요.
  • 404 INVALID_USER — 매체측 user_id 를 찾을 수 없음. 재시도.
  • 409 DUPLICATE_POSTBACK — 매체가 명시적으로 중복 거부. 성공으로 간주, 재시도 중단.
  • 5xx INTERNAL_ERROR — 일시적 장애. 재시도.
멱등 권고
동일 postback_id 중복 수신은 반드시 HTTP 2xx 로 응답하여 Blomics 재시도 사이클을 차단하고 중복 적립을 방지하세요. Blomics 워커는 10초 타임아웃 내 응답이 없으면 abort 합니다.

구현 시 주의사항

  • postback_id 로 중복 적립을 방지해야 합니다. 재시도 시 동일한 ID 가 올 수 있습니다.
  • HMAC 서명을 검증하여 요청 무결성을 확인해야 합니다.
  • 적립 처리 후 빠르게 응답해야 합니다. 응답 지연 시 타임아웃됩니다 (기본 10초).
  • 매체사가 게이트웨이 호출 시 동봉하는 callback_data 는 Blomics 포스트백에서 동일한 JSON 문자열로 되돌아옵니다.

HMAC 서명 검증

포스트백 요청에 포함된 HMAC 서명을 검증하여 요청이 Blomics 서버에서 전송된 것인지 확인합니다.

  • Blomics 에서 발급받은 hmac_secret 으로 요청 파라미터를 서명합니다.
  • 요청의 hmac 값과 직접 계산한 서명값을 비교합니다. 불일치 시 위조 요청이므로 거부합니다.

서명 알고리즘

  1. payload 에서 hmac 키 제거
  2. 모든 중첩 레벨의 객체 키를 유니코드 순으로 정렬해 canonical JSON 직렬화 (배열은 순서 유지)
  3. HMAC-SHA256 (hmac_secret) 으로 서명 생성 (base64 인코딩)
  4. 요청의 hmac 값과 비교 (timing-safe)

TypeScript (Node.js 18+)

tsx
import { createHmac, timingSafeEqual } from "node:crypto";

function canonicalize(v: unknown): string {
  if (typeof v === "undefined") return "null";
  if (v === null || typeof v !== "object") return JSON.stringify(v);
  if (Array.isArray(v)) return "[" + v.map(canonicalize).join(",") + "]";
  const o = v as Record<string, unknown>;
  const keys = Object.keys(o).sort();
  return "{" + keys.map(k => JSON.stringify(k) + ":" + canonicalize(o[k])).join(",") + "}";
}

export function signHmac(payload: Record<string, unknown>, secret: string): string {
  const { hmac: _omit, ...rest } = payload;
  return createHmac("sha256", secret).update(canonicalize(rest)).digest("base64");
}

export function verifyHmac(payload: Record<string, unknown>, secret: string, received: string): boolean {
  const expected = signHmac(payload, secret);
  const a = Buffer.from(expected, "base64");
  const b = Buffer.from(received, "base64");
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}

// 사용 예 (매체 수신 엔드포인트)
const body = await request.json();
if (!verifyHmac(body, process.env.BLOMICS_HMAC_SECRET!, body.hmac)) {
  return new Response(JSON.stringify({ code: "HMAC_MISMATCH" }), { status: 401 });
}

Java (JDK 17+, Jackson jackson-databind) — Part 1: 클래스 및 sign/verify

java
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public final class BlomicsHmac {
  private static final ObjectMapper M = new ObjectMapper();

  public static String sign(Map<String, Object> payload, String secret) throws Exception {
    Map<String, Object> copy = new LinkedHashMap<>(payload);
    copy.remove("hmac");
    String canonical = canonicalize(M.valueToTree(copy));
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    return Base64.getEncoder().encodeToString(
      mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8))
    );
  }

  public static boolean verify(Map<String, Object> payload, String secret, String received) throws Exception {
    byte[] a = Base64.getDecoder().decode(sign(payload, secret));
    byte[] b = Base64.getDecoder().decode(received);
    if (a.length != b.length) return false;
    int diff = 0;
    for (int i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
    return diff == 0;  // constant-time compare
  }

Java — Part 2: canonicalize 메서드 (위 클래스의 } 닫기 전에 이어 붙여 사용)

java
  private static String canonicalize(JsonNode n) throws Exception {
    if (n == null || n.isNull()) return "null";
    if (n.isObject()) {
      List<String> keys = new ArrayList<>();
      n.fieldNames().forEachRemaining(keys::add);
      Collections.sort(keys);
      StringBuilder sb = new StringBuilder("{");
      for (int i = 0; i < keys.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append(M.writeValueAsString(keys.get(i)))
          .append(":")
          .append(canonicalize(n.get(keys.get(i))));
      }
      return sb.append("}").toString();
    }
    if (n.isArray()) {
      StringBuilder sb = new StringBuilder("[");
      for (int i = 0; i < n.size(); i++) {
        if (i > 0) sb.append(",");
        sb.append(canonicalize(n.get(i)));
      }
      return sb.append("]").toString();
    }
    if (n.isTextual()) return M.writeValueAsString(n.textValue());
    return n.toString();   // numbers, booleans
  }
}

리포트 및 랭킹 API

리포트

활성 사용자 및 페이지 뷰 통계를 조회합니다.

GET/api/v1/reports/users

활성 사용자 리포트 — 사용자/매체별 집계.

GET/api/v1/reports/views

페이지 뷰 리포트 — 조회/세션 집계.

랭킹

실시간 또는 전체 랭킹 목록을 조회합니다.

GET/api/v1/campaigns/{campaign_id}/rankings/live

실시간 랭킹 — 진행 중인 캠페인.

GET/api/v1/campaigns/{campaign_id}/rankings

확정 랭킹 스냅샷 조회 (캠페인 종료 후).

사용자

개인정보를 파기합니다.

DELETE/api/v1/users/{publisher_user_id}

매체사 사용자 삭제 — 활동 PII 파기.

연동 체크리스트

필수

  • ☐ 게이트웨이 URL 을 웹뷰로 호출하는 동작 확인
  • ☐ 포스트백 수신 API 구현 완료
  • postback_id 기반 중복 적립 방지 로직
  • ☐ HMAC 서명 검증 로직
  • ☐ 성공 시 HTTP 2xx 응답 반환

선택

  • ☐ 인증 API 구현 (token 방식)
  • ☐ 웹뷰 브릿지 (BlomicsBridge) 주입
  • /bridge-test 페이지로 검증