개발 가이드
매체사 연동
본 문서는 Blomics 가 제공하는 게이미피케이션 컨텐츠를 매체사 앱에 연동하기 위한 기술 가이드입니다. 매체사는 이 문서를 참고하여 인증 API, 포스트백 API, 웹뷰 브릿지를 구현해주시기 바랍니다.
연동 전 준비사항
Blomics 연동을 시작하기 전에 다음 정보를 Blomics 담당자로부터 전달받아야 합니다.
publisher_id— 매체사 식별값 (UUID)publisher_secret— 매체사 API 인증 비밀키. 32-byte base64url(43자). 등록 시 1회 자동 발급content_id[]— 연동할 콘텐츠 식별값(UUID) 목록. Blomics 운영팀에 요청hmac_secret— 포스트백 서명 검증용 비밀키auth_type—plain(직접 전달) 또는token(토큰 교환)
매체사가 준비해야 할 것
- 게이트웨이 URL 연동 — 블로믹스 컨텐트 페이지로 리다이렉트 (필수)
- 인증 API (
auth_url) — token 인증 방식 사용 시 (선택) - 포스트백 수신 API (
postback_url) — Blomics 가 포인트 적립을 요청 (필수) - 웹뷰 브릿지 (
BlomicsBridge) — 매체사 앱의 웹뷰에 JavaScript 브릿지 객체를 주입 (권장)
Authorization: Basic base64(publisher_id:publisher_secret) 또는Authorization: Bearer <publisher_secret> 둘 다 지원합니다. 신규 통합은 Bearer 권장 — 자세한 내용은 API Reference.게이트웨이 URL 연동
매체사 앱의 웹뷰 또는 네이티브 화면에서 아래 URL 을 호출하면 Blomics 서버가 사용자를 인증하고 콘텐츠 페이지로 자동 리다이렉트합니다.
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-000000000000callback_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 → 매체사)
POST {auth_url}
Content-Type: application/json
{ "token": "<일회성 토큰>" }응답 (매체사 → Blomics)
HttpStatusCode 2xx 을 성공으로 처리하고 그 외에는 모두 실패로 처리합니다. 실패 시 1회 재시도 합니다.
{ "user_id": "매체측 사용자 ID" }포스트백 API 구현 (필수)
사용자가 리워드 조건을 달성하면 Blomics 서버가 매체사의 포스트백 API 를 호출합니다. 사용자가 리워드 조건을 달성하지 못한 이벤트에 대해서도 포스트백을 받기 위해서는 매체사 속성의 send_postback_on_reject 를 활성화해야 합니다.
요청 (Blomics → 매체사)
postback_id— 고유 식별값. 재시도 시 동일 ID 가 올 수 있어 중복 적립을 방지해야 함user_id— 게이트웨이 URL 로 전달한 매체사 사용자 IDcontent_id— 콘텐츠 식별값campaign_id— 캠페인 식별값campaign_name— 캠페인 이름event_id— 이벤트 식별값event_type— 이벤트 유형event_name— 이벤트 이름campaign_type—always_on또는promotionparent_campaign_id— promotion 의 경우 부모 캠페인 UUIDpoint— 지급할 포인트total_point— 캠페인 동안 지급할 전체 포인트created_at— 포스트백 요청 시간hmac— HMAC 서명값callback_data— 게이트웨이 URL 로 전달한 값
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.status를success로 마킹합니다.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값과 직접 계산한 서명값을 비교합니다. 불일치 시 위조 요청이므로 거부합니다.
서명 알고리즘
- payload 에서
hmac키 제거 - 모든 중첩 레벨의 객체 키를 유니코드 순으로 정렬해 canonical JSON 직렬화 (배열은 순서 유지)
- HMAC-SHA256 (
hmac_secret) 으로 서명 생성 (base64 인코딩) - 요청의
hmac값과 비교 (timing-safe)
TypeScript (Node.js 18+)
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
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 메서드 (위 클래스의 } 닫기 전에 이어 붙여 사용)
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
리포트
활성 사용자 및 페이지 뷰 통계를 조회합니다.
/api/v1/reports/users활성 사용자 리포트 — 사용자/매체별 집계.
/api/v1/reports/views페이지 뷰 리포트 — 조회/세션 집계.
랭킹
실시간 또는 전체 랭킹 목록을 조회합니다.
/api/v1/campaigns/{campaign_id}/rankings/live실시간 랭킹 — 진행 중인 캠페인.
/api/v1/campaigns/{campaign_id}/rankings확정 랭킹 스냅샷 조회 (캠페인 종료 후).
사용자
개인정보를 파기합니다.
/api/v1/users/{publisher_user_id}매체사 사용자 삭제 — 활동 PII 파기.
연동 체크리스트
필수
- ☐ 게이트웨이 URL 을 웹뷰로 호출하는 동작 확인
- ☐ 포스트백 수신 API 구현 완료
- ☐
postback_id기반 중복 적립 방지 로직 - ☐ HMAC 서명 검증 로직
- ☐ 성공 시 HTTP 2xx 응답 반환
선택
- ☐ 인증 API 구현 (token 방식)
- ☐ 웹뷰 브릿지 (
BlomicsBridge) 주입 - ☐
/bridge-test페이지로 검증