본문으로 건너뛰기
Unveilydocs

IAP API 연동 가이드

Unveily IAP API를 사용한 서버 사이드 인앱결제 검증 설정 — SDK 불필요.

한눈에 보기

Unveily IAP API는 고객사의 백엔드 서버에서 Google Play 및 App Store 구매 영수증을 검증합니다. 앱은 Google Play Billing / StoreKit 2로 결제를 처리하고, 서버가 우리 API를 호출하여 영수증을 검증합니다.

Android 앱  ──purchaseToken──▶          고객 서버  ──googleAccessToken──▶  Unveily API  ──▶  Google Play
iOS 앱      ──signedTransaction(JWS)──▶  고객 서버  ─────────────────────▶  Unveily API  ──▶  App Store
항목필요 조건
Unveily 플랜IAP API 구독
Google Play개발자 계정 + 결제 프로필 설정 완료
인프라백엔드 서버 (언어·클라우드 무관)

시작 전 준비

서비스 계정 키 보안

서비스 계정 JSON 키는 마스터 자격증명입니다

이 키는 Google Play Console의 모든 주문 데이터에 대한 읽기 권한을 가집니다. 비밀번호와 동일하게 취급하세요 — 소스코드에 포함하거나 평문으로 공유하지 마세요.

인프라 환경에 맞는 시크릿 관리 도구에 키를 보관하세요:

인프라 환경권장 보관 방법
AWSAWS Secrets Manager 또는 Systems Manager Parameter Store (SecureString)
Google CloudGCP Secret Manager
AzureAzure Key Vault
자체 서버 (On-premises)HashiCorp Vault 권장 — 불가 시 OS 환경변수로 격리 보관
공통 금지 사항소스코드 하드코딩, Git 커밋, 프로덕션에서 평문 .env 사용 절대 금지

Mission 1 — Google Cloud 서비스 계정 생성

1-1. Google Play Android Developer API 활성화

  1. Google Cloud Console 접속 → Play 계정과 연결된 프로젝트 선택
  2. API 및 서비스API 및 서비스 사용 설정
  3. Google Play Android Developer API 검색 → 사용 설정

1-2. 서비스 계정 생성

  1. IAM 및 관리자서비스 계정서비스 계정 만들기
  2. 이름 입력 (예: unveily-iap-verifier) → 만들고 계속하기
  3. 역할 부여 단계는 건너뜀 → 완료

1-3. JSON 키 다운로드

  1. 생성된 서비스 계정 클릭 →
  2. 키 추가새 키 만들기JSON만들기
  3. JSON 파일이 자동으로 다운로드됩니다 — 즉시 시크릿 관리 도구에 보관하세요

최초 1회만 다운로드 가능

Google은 키 생성 시점에만 다운로드를 허용합니다. 분실 시 해당 키를 삭제하고 새로 생성해야 합니다.


Mission 2 — Google Play Console 권한 연결

2-1. 액세스 권한 부여

  1. Google Play Console설정API 액세스
  2. Google Cloud 프로젝트와 연결 (미연결 시)
  3. 서비스 계정 목록에서 생성한 계정 찾기 → 액세스 권한 부여

2-2. 최소 권한만 설정

최소 권한 원칙

구매 검증에 필요한 권한만 부여하세요 — 그 이상은 부여하지 않습니다.

권한필요 여부이유
재무 데이터 보기필수구매 및 구독 상태 조회
주문 및 구독 관리필수purchases API 호출에 필요
그 외 모든 권한불필요활성화하지 마세요
  1. 저장 → 권한 반영까지 최대 24시간 소요

Mission 3 — 서버에서 Access Token 발급

서버는 시크릿 관리 도구에서 JSON 키를 읽어 단기 유효 Google OAuth2 Access Token(유효시간 1시간)을 생성합니다. JSON 키가 아닌 이 토큰만 Unveily에 전달합니다.

키가 아닌 토큰을 전달하는 이유

JSON 키는 만료되지 않는 영구 자격증명입니다. Access Token은 1시간 후 만료됩니다. HTTPS로 전송 중에 설령 노출되더라도 피해 시간이 엄격히 제한됩니다. Unveily는 토큰을 저장하지 않습니다 — 요청당 1회 사용 후 폐기합니다.

Node.js

import { GoogleAuth } from 'google-auth-library';

const auth = new GoogleAuth({
  // 시크릿 관리 도구에서 로드 — 절대 하드코딩하지 마세요
  credentials: JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON),
  scopes: ['https://www.googleapis.com/auth/androidpublisher'],
});

async function getGoogleAccessToken() {
  const client = await auth.getClient();
  const tokenResponse = await client.getAccessToken();
  return tokenResponse.token; // "ya29.xxxx..."
}
npm install google-auth-library

Python

from google.oauth2 import service_account
import google.auth.transport.requests
import json, os

def get_google_access_token() -> str:
    key_data = json.loads(os.environ["GOOGLE_SERVICE_ACCOUNT_JSON"])
    credentials = service_account.Credentials.from_service_account_info(
        key_data,
        scopes=["https://www.googleapis.com/auth/androidpublisher"],
    )
    request = google.auth.transport.requests.Request()
    credentials.refresh(request)
    return credentials.token  # "ya29.xxxx..."
pip install google-auth

Java

import com.google.auth.oauth2.GoogleCredentials;
import java.io.ByteArrayInputStream;
import java.util.Collections;

public String getGoogleAccessToken() throws Exception {
    String keyJson = System.getenv("GOOGLE_SERVICE_ACCOUNT_JSON");
    GoogleCredentials credentials = GoogleCredentials
        .fromStream(new ByteArrayInputStream(keyJson.getBytes()))
        .createScoped(Collections.singletonList(
            "https://www.googleapis.com/auth/androidpublisher"
        ));
    credentials.refreshIfExpired();
    return credentials.getAccessToken().getTokenValue(); // "ya29.xxxx..."
}
<!-- pom.xml -->
<dependency>
  <groupId>com.google.auth</groupId>
  <artifactId>google-auth-library-oauth2-http</artifactId>
  <version>1.23.0</version>
</dependency>

.NET (C#)

using Google.Apis.Auth.OAuth2;

public async Task<string> GetGoogleAccessTokenAsync()
{
    var keyJson = Environment.GetEnvironmentVariable("GOOGLE_SERVICE_ACCOUNT_JSON")
        ?? throw new InvalidOperationException("GOOGLE_SERVICE_ACCOUNT_JSON 환경변수가 설정되지 않았습니다.");

    var credential = GoogleCredential
        .FromJson(keyJson)
        .CreateScoped("https://www.googleapis.com/auth/androidpublisher");

    return await credential.UnderlyingCredential
        .GetAccessTokenForRequestAsync(); // "ya29.xxxx..."
}
dotnet add package Google.Apis.Auth

Mission 4 — 검증 API 호출

Access Token을 발급한 후 서버에서 Unveily IAP API를 호출합니다:

POST https://api.theunveily.com/api/iap/verify
Authorization: Bearer {UNVEILY_LICENSE_KEY}
Content-Type: application/json

{
  "googleAccessToken": "ya29.xxxx...",
  "purchaseToken": "AO-J1OxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxBxxxxxx",
  "packageName": "com.example.myapp",
  "productId": "premium_monthly",
  "productType": "subs",
  "platform": "android"
}
필드필수설명
Authorization 헤더필수Bearer {UNVEILY_LICENSE_KEY}
googleAccessToken필수서비스 계정에서 발급한 단기 OAuth2 토큰
purchaseToken필수기기에서 Google Play Billing이 반환한 토큰
packageName필수앱 패키지명 (예: com.example.myapp)
productId필수Play Console에 등록된 상품 ID
productType필수inapp 또는 subs
platform선택기본값 android

성공 응답

{
  "success": true,
  "data": {
    "success": true,
    "platform": "android",
    "productId": "premium_monthly",
    "orderId": "GPA.1234-5678-9012-34567",
    "purchaseTime": "2025-05-01T09:23:11Z",
    "purchaseState": 0,
    "isAcknowledged": false,
    "storeApiVerified": true
  }
}

오류 응답

HTTP오류조치
401Authorization 헤더 없음 또는 형식 오류Bearer {licenseKey} 형식 확인
400googleAccessToken 누락호출 전 토큰 발급 필요
400Play API 검증 실패토큰 만료 또는 서비스 계정 Play Console 권한 미설정
400이미 검증된 영수증동일 purchaseToken 중복 처리 — 무시해도 안전
400유효하지 않은 라이선스 키대시보드에서 라이선스 키 및 구독 상태 확인

iOS / App Store 변형

iOS는 Google 서비스 계정이나 googleAccessToken이 필요 없습니다. StoreKit 2가 반환한 Apple 서명 JWS(signedTransaction)를 그대로 전달하면, Unveily 서버가 Apple 공개키로 서명을 검증합니다. platformios로 설정하세요.

POST https://api.theunveily.com/api/iap/verify
Authorization: Bearer {UNVEILY_LICENSE_KEY}
Content-Type: application/json

{
  "platform": "ios",
  "signedTransaction": "eyJhbGciOiJFUzI1NiIsIng1YyI6...",
  "productId": "premium_monthly",
  "productType": "subs"
}
필드필수설명
Authorization 헤더필수Bearer {UNVEILY_LICENSE_KEY}
platform필수iOS는 반드시 ios
signedTransaction필수StoreKit 2가 반환한 Apple 서명 JWS (purchaseToken + googleAccessToken를 대체)
productId필수App Store Connect에 등록된 상품 ID
productType필수inapp 또는 subs

iOS는 googleAccessToken·packageName 불필요

JWS 자체에 번들 ID와 상품 정보가 서명되어 포함되므로, iOS에서는 googleAccessTokenpackageName을 보내지 않습니다. Unveily가 Apple 공개키로 서명을 직접 검증합니다.

iOS 성공 응답

{
  "success": true,
  "data": {
    "success": true,
    "platform": "ios",
    "productId": "premium_monthly",
    "transactionId": "2000000012345678",
    "purchaseTime": "2025-05-01T09:23:11Z",
    "storeApiVerified": true
  }
}

Mission 5 — 전체 플로우 구현 예시

// Node.js / Express 예시
import axios from 'axios';
import { getGoogleAccessToken } from './googleAuth'; // 위에서 작성한 헬퍼

app.post('/purchase/verify', async (req, res) => {
  const { purchaseToken, productId, productType } = req.body;

  // 1. 새 Google Access Token 발급
  const googleAccessToken = await getGoogleAccessToken();

  // 2. Unveily IAP API 호출
  const response = await axios.post(
    'https://api.theunveily.com/api/iap/verify',
    {
      googleAccessToken,
      purchaseToken,
      packageName: 'com.example.myapp',
      productId,
      productType,
      platform: 'android',
    },
    {
      headers: {
        Authorization: `Bearer ${process.env.UNVEILY_LICENSE_KEY}`,
        'Content-Type': 'application/json',
      },
    }
  );

  if (response.data.success) {
    // 3. 구매한 기능 활성화
    await activateFeature(req.user.id, productId);
    res.json({ success: true });
  } else {
    res.status(400).json({ success: false, error: response.data.error });
  }
});

완료 전 점검

  • 서비스 계정 JSON 키를 시크릿 관리 도구에 보관 (코드·.env 파일 제외)
  • Play Console 권한: 재무 데이터 보기 + 주문 및 구독 관리 만 활성화
  • Access Token을 검증 호출 직전 서버에서 발급
  • 모든 요청에 Authorization: Bearer {licenseKey} 헤더 포함
  • 기기 → 서버 간 purchaseToken 전송은 HTTPS로만
  • 응답 storeApiVerified: true 확인 (Google Play 검증 완료)
  • 중복 영수증 오류(400 이미 검증된 영수증) 정상 처리

다음 여정

On this page