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의 모든 주문 데이터에 대한 읽기 권한을 가집니다. 비밀번호와 동일하게 취급하세요 — 소스코드에 포함하거나 평문으로 공유하지 마세요.
인프라 환경에 맞는 시크릿 관리 도구에 키를 보관하세요:
| 인프라 환경 | 권장 보관 방법 |
|---|---|
| AWS | AWS Secrets Manager 또는 Systems Manager Parameter Store (SecureString) |
| Google Cloud | GCP Secret Manager |
| Azure | Azure Key Vault |
| 자체 서버 (On-premises) | HashiCorp Vault 권장 — 불가 시 OS 환경변수로 격리 보관 |
| 공통 금지 사항 | 소스코드 하드코딩, Git 커밋, 프로덕션에서 평문 .env 사용 절대 금지 |
Mission 1 — Google Cloud 서비스 계정 생성
1-1. Google Play Android Developer API 활성화
- Google Cloud Console 접속 → Play 계정과 연결된 프로젝트 선택
- API 및 서비스 → API 및 서비스 사용 설정
- Google Play Android Developer API 검색 → 사용 설정
1-2. 서비스 계정 생성
- IAM 및 관리자 → 서비스 계정 → 서비스 계정 만들기
- 이름 입력 (예:
unveily-iap-verifier) → 만들고 계속하기 - 역할 부여 단계는 건너뜀 → 완료
1-3. JSON 키 다운로드
- 생성된 서비스 계정 클릭 → 키 탭
- 키 추가 → 새 키 만들기 → JSON → 만들기
- JSON 파일이 자동으로 다운로드됩니다 — 즉시 시크릿 관리 도구에 보관하세요
최초 1회만 다운로드 가능
Google은 키 생성 시점에만 다운로드를 허용합니다. 분실 시 해당 키를 삭제하고 새로 생성해야 합니다.
Mission 2 — Google Play Console 권한 연결
2-1. 액세스 권한 부여
- Google Play Console → 설정 → API 액세스
- Google Cloud 프로젝트와 연결 (미연결 시)
- 서비스 계정 목록에서 생성한 계정 찾기 → 액세스 권한 부여
2-2. 최소 권한만 설정
최소 권한 원칙
구매 검증에 필요한 권한만 부여하세요 — 그 이상은 부여하지 않습니다.
| 권한 | 필요 여부 | 이유 |
|---|---|---|
| 재무 데이터 보기 | 필수 | 구매 및 구독 상태 조회 |
| 주문 및 구독 관리 | 필수 | purchases API 호출에 필요 |
| 그 외 모든 권한 | 불필요 | 활성화하지 마세요 |
- 저장 → 권한 반영까지 최대 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-libraryPython
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-authJava
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.AuthMission 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 | 오류 | 조치 |
|---|---|---|
401 | Authorization 헤더 없음 또는 형식 오류 | Bearer {licenseKey} 형식 확인 |
400 | googleAccessToken 누락 | 호출 전 토큰 발급 필요 |
400 | Play API 검증 실패 | 토큰 만료 또는 서비스 계정 Play Console 권한 미설정 |
400 | 이미 검증된 영수증 | 동일 purchaseToken 중복 처리 — 무시해도 안전 |
400 | 유효하지 않은 라이선스 키 | 대시보드에서 라이선스 키 및 구독 상태 확인 |
iOS / App Store 변형
iOS는 Google 서비스 계정이나 googleAccessToken이 필요 없습니다. StoreKit 2가 반환한 Apple 서명 JWS(signedTransaction)를 그대로 전달하면, Unveily 서버가 Apple 공개키로 서명을 검증합니다. platform을 ios로 설정하세요.
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에서는 googleAccessToken과 packageName을 보내지 않습니다. 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 이미 검증된 영수증) 정상 처리
다음 여정
- IAP Bridge API — Pro 플랜 SDK 기반 IAP
- 라이선스 설정 — 라이선스 키 설정