커넥트 API 연동 가이드
AI Market 밖에서 운영하는 서비스가 AI Market 계정으로 사용자를 가입·로그인시키고, 그 사용자가 서비스를 구독 중인지 확인하며, 사용자별 토큰·크레딧 사용량을 기록하는 방법을 순서대로 설명합니다. 사용자를 AI Market 승인 화면으로 보내 연동을 승인받은 후, 서버에서 API 키와 사용자 ID로 구독 상태를 조회하고 사용량을 보고합니다.
개요
커넥트 API는 OAuth 2.0 인가 코드 방식(PKCE 지원)을 따릅니다. 전체 흐름은 다음 6개의 요청으로 이루어집니다.
1 브라우저 GET /connect/authorize?client_id&redirect_uri&service_id&scope&state → 로그인 후 승인
2 브라우저 → {redirect_uri}?code=nxac_…&state=… (취소 시 ?error=access_denied)
3 서버 POST /api/v1/connect/token grant_type=authorization_code → 접근 토큰·갱신 토큰
4 서버 GET /api/v1/connect/me Authorization: Bearer nxa_… → 로그인한 사용자 정보(사용자 ID)
5 서버 GET /api/v1/connect/subscription API 키 + userId · serviceId → 해당 사용자의 실시간 구독 확인
6 서버 POST /api/v1/connect/usage API 키 + {"userId", "serviceId", "amount"} → 해당 사용자의 사용량 보고(월 제공량이 있는 서비스)
커넥트 API의 요청은 모두 서비스를 운영하는 크리에이터의 서버에서 전송합니다. 서비스의 사용자가 직접 호출하는 API가 아니며, 사용자의 브라우저는 승인 화면으로 이동할 때만 사용됩니다.
구독 확인과 사용량 보고는 서버가 대상 사용자를 지정해서 호출합니다. 어느 사용자의 구독인지, 어느 사용자가 얼마를 사용했는지를 userId로 전달합니다.
준비물은 3가지입니다. 크리에이터 계정(크리에이터 모드), 연동할 서비스(테스트 키는 임시저장 상태도 가능), 그리고 클라이언트 시크릿을 안전하게 보관하고 토큰을 교환할 서버입니다. 클라이언트 시크릿이 필요한 요청은 브라우저나 모바일 앱에서 직접 보내지 않습니다.
코드를 작성하기 전에 샌드박스에서 테스트 키로 전체 흐름을 실행해 보세요. 실제 요청·응답 형식과 오류 코드를 바로 확인할 수 있습니다.
AI Market 계정으로 간편 가입·로그인 붙이기
카카오·네이버 로그인처럼, 서비스의 가입·로그인 화면에 [AI Market 계정으로 로그인] 버튼을 두는 방법입니다. 사용자는 서비스에 아이디와 비밀번호를 만들지 않고 AI Market 계정으로 가입하고 로그인하며, 서비스는 AI Market이 확인한 사용자 ID·이메일·이름·닉네임을 받아 회원을 만듭니다.
AI Market 계정이 없는 사용자는 승인 과정에서 AI Market 가입을 마치고 자동으로 서비스로 돌아오므로, 서비스 쪽에는 가입 화면이 따로 필요 없습니다.
이 절은 처음부터 끝까지 한 번에 읽히도록 썼습니다. 서비스가 만들 것은 버튼 하나, 콜백 주소 하나, 회원 표의 사용자 ID 열 하나입니다. 구독 확인과 사용량 보고까지 필요하면 같은 승인 요청에 service_id를 더하고 7절·8절을 이어서 보세요.
사용자에게 보이는 흐름
사용자가 서비스의 가입·로그인 화면에서 [AI Market 계정으로 로그인]을 누릅니다. 버튼은 서비스의 시작 주소(예: /auth/aimarket/start)를 엽니다.
위조 요청을 막을 state 값을 만들어 세션에 저장하고, 사용자를 AI Market 승인 요청 주소(/connect/authorize)로 이동시킵니다. 로그인만 연동하면 service_id는 넣지 않습니다.
로그인하지 않은 사용자는 AI Market 로그인 화면을 거칩니다. AI Market 계정이 없으면 그 자리에서 회원가입(이메일·카카오·네이버·Google)을 마치고 자동으로 이어집니다.
승인 화면에 서비스 이름과 제공되는 정보(회원 번호와 동의 항목)가 표시되고, 사용자가 [동의하고 계속하기]를 누릅니다. 이미 동의한 사용자는 이 화면 없이 바로 다음 단계로 갑니다.
서비스의 콜백 주소로 돌아옵니다. 주소 뒤에 code(인가 코드)와 보냈던 state가 붙어 있습니다. 사용자가 취소하면 error=access_denied가 옵니다.
응답의 id(사용자 ID)로 서비스의 회원을 찾습니다. 없으면 받은 정보로 회원을 만들고(간편 가입), 있으면 그 회원으로 로그인시킵니다. 세션을 만들고 서비스 화면으로 보내면 끝입니다.
두 번째 방문부터는 3단계의 승인 화면이 나오지 않습니다. 사용자가 버튼을 누르면 콜백으로 바로 돌아오고, 서비스는 같은 콜백 코드로 기존 회원을 로그인시킵니다. 다른 브라우저나 기기에서도 AI Market 로그인만 거치면 같은 사용자 ID가 돌아옵니다.
서비스가 구현할 것
| 구현 항목 | 하는 일 | 참고 |
|---|---|---|
| ① 시작 주소 예: GET /auth/aimarket/start | 버튼이 여는 주소입니다. state를 만들어 세션에 저장하고 승인 요청 주소로 302 리다이렉트합니다. client_secret은 쓰지 않습니다. | 4절 승인 요청, 버튼 만들기 |
| ② 콜백 주소 예: GET /auth/aimarket/callback | state 확인 → 토큰 교환 → 사용자 정보 조회 → 회원 찾기 또는 만들기 → 로그인 세션 발급. 이 주소를 API 키 › 리다이렉트 URI에 미리 등록합니다. | 5절, 6절 |
| ③ 회원 표의 사용자 ID 열 예: aimarket_user_id | AI Market 사용자 ID(UUID 문자열)를 저장하는 고유(UNIQUE) 열입니다. 회원을 찾는 기준은 언제나 이 값이며, 이메일이 아닙니다. | 아래 '받는 정보와 회원 정보' |
준비물은 API 키(운영 키·테스트 키)와 등록한 리다이렉트 URI뿐입니다. 서버는 어떤 언어든 상관없고, HTTPS 요청 두 개(토큰 교환·사용자 정보 조회)를 보낼 수 있으면 됩니다.
받는 정보와 회원 정보
| AI Market 응답 | 동의 항목 | 서비스 회원 정보로 | 비고 |
|---|---|---|---|
id | 항상 제공 | 사용자 ID 열(필수, 고유) | 계정마다 고정된 값이며 변경되지 않습니다. 회원을 구분하는 기준 |
email, emailVerified | email | 이메일, 이메일 인증 여부 | 사용자가 바꿀 수 있습니다. emailVerified가 true인 이메일만 인증된 것으로 다루세요 |
name | name | 이름 | 본인인증을 마친 계정은 인증된 이름입니다 |
nickname, profileImageUrl | profile | 닉네임, 프로필 사진 | 사진은 AI Market 주소를 그대로 쓰거나 복사해 둡니다 |
- 동의하지 않은 항목은
null로 옵니다. 서비스에 꼭 필요한 항목은 승인 요청의scope에 넣고, 그래도 비어 있으면 가입 직후 화면에서 직접 입력받으세요. - 휴대폰 번호·생년월일·주소는 제공하지 않습니다. 필요하면 서비스가 가입 직후 입력받습니다.
- 서비스에 이미 같은 이메일로 가입한 회원이 있을 수 있습니다.
emailVerified가true이면 그 회원에 사용자 ID를 붙여 로그인시키고, 아니면 "이미 가입된 이메일입니다. 기존 방법으로 로그인한 뒤 설정에서 AI Market 계정을 연결하세요"로 안내하는 것을 권장합니다(계정 연결).
전체 예제
시작 주소와 콜백 주소를 한 파일에 담은 예제입니다. 그대로 실행되는 수준으로 적었고, 회원 저장소(db.users, User)만 서비스의 것으로 바꾸면 됩니다.
// npm i express express-session
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const AIMARKET = 'https://ai-market.co.kr';
const CLIENT_ID = process.env.AIMARKET_CLIENT_ID; // nxc_… (API 키 화면)
const CLIENT_SECRET = process.env.AIMARKET_CLIENT_SECRET; // nxs_… — 서버 환경 변수에만
const REDIRECT_URI = 'https://myservice.com/auth/aimarket/callback'; // API 키 › 리다이렉트 URI 에 등록한 주소
const app = express();
app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false }));
// ① 시작 주소 — [AI Market 계정으로 로그인] 버튼이 연다 → 승인 화면으로 보낸다
app.get('/auth/aimarket/start', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.aimarketState = state;
const q = new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, scope: 'profile email name', state });
res.redirect(`${AIMARKET}/connect/authorize?${q}`); // 구독까지 연동하면 service_id 를 더한다
});
// ② 콜백 주소 — state 확인 → 토큰 교환 → 사용자 정보 → 회원 찾기/만들기 → 로그인
app.get('/auth/aimarket/callback', async (req, res) => {
const { code, state, error } = req.query;
if (error) return res.redirect('/login?cancelled=1'); // 사용자가 취소(access_denied)
if (!code || state !== req.session.aimarketState) return res.status(400).send('잘못된 요청입니다.');
delete req.session.aimarketState;
const tokenRes = await fetch(`${AIMARKET}/api/v1/connect/token`, { // 5절 토큰 교환
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ grant_type: 'authorization_code', code, redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID, client_secret: CLIENT_SECRET }),
});
if (!tokenRes.ok) return res.status(502).send('로그인에 실패했습니다. 다시 시도해 주세요.');
const { access_token } = await tokenRes.json();
const meRes = await fetch(`${AIMARKET}/api/v1/connect/me`, { // 6절 사용자 정보 조회
headers: { Authorization: `Bearer ${access_token}` },
});
const me = await meRes.json(); // { id, email, emailVerified, name, nickname, profileImageUrl, … }
let user = await db.users.findOne({ aimarketUserId: me.id }); // ③ 사용자 ID 로 회원 조회
if (!user) {
user = await db.users.insert({ // 처음이면 간편 가입
aimarketUserId: me.id, email: me.email, emailVerified: me.emailVerified,
name: me.name, nickname: me.nickname, avatarUrl: me.profileImageUrl,
});
}
req.session.userId = user.id; // ④ 로그인
res.redirect('/');
});
app.listen(3000);
Python (Flask) 예제
# pip install flask requests
import os, secrets, requests
from urllib.parse import urlencode
from flask import Flask, redirect, request, session, abort
AIMARKET = 'https://ai-market.co.kr'
CLIENT_ID = os.environ['AIMARKET_CLIENT_ID'] # nxc_… (API 키 화면)
CLIENT_SECRET = os.environ['AIMARKET_CLIENT_SECRET'] # nxs_… — 서버 환경 변수에만
REDIRECT_URI = 'https://myservice.com/auth/aimarket/callback'
app = Flask(__name__)
app.secret_key = os.environ['SESSION_SECRET']
@app.get('/auth/aimarket/start') # ① 시작 주소 — 버튼이 연다
def start():
state = secrets.token_hex(16)
session['aimarket_state'] = state
q = urlencode({'client_id': CLIENT_ID, 'redirect_uri': REDIRECT_URI, 'scope': 'profile email name', 'state': state})
return redirect(f'{AIMARKET}/connect/authorize?{q}') # 구독까지 연동하면 service_id 를 더한다
@app.get('/auth/aimarket/callback') # ② 콜백 주소
def callback():
if request.args.get('error'): # 사용자가 취소(access_denied)
return redirect('/login?cancelled=1')
if request.args.get('state') != session.pop('aimarket_state', None):
abort(400)
token = requests.post(f'{AIMARKET}/api/v1/connect/token', data={ # 5절 토큰 교환
'grant_type': 'authorization_code', 'code': request.args['code'], 'redirect_uri': REDIRECT_URI,
'client_id': CLIENT_ID, 'client_secret': CLIENT_SECRET}, timeout=10)
token.raise_for_status()
me = requests.get(f'{AIMARKET}/api/v1/connect/me', # 6절 사용자 정보 조회
headers={'Authorization': f"Bearer {token.json()['access_token']}"}, timeout=10).json()
user = User.query.filter_by(aimarket_user_id=me['id']).first() # ③ 사용자 ID 로 회원 조회
if user is None: # 처음이면 간편 가입
user = User(aimarket_user_id=me['id'], email=me.get('email'), email_verified=me.get('emailVerified'),
name=me.get('name'), nickname=me.get('nickname'), avatar_url=me.get('profileImageUrl'))
db.session.add(user)
db.session.commit()
session['user_id'] = user.id # ④ 로그인
return redirect('/')
client_secret은 서버의 환경 변수에만 둡니다. 브라우저 코드·모바일 앱·저장소에 넣지 마세요.redirect_uri는 등록한 값과 한 글자도 다르지 않아야 합니다(2절). 개발용 주소와 운영 주소를 모두 등록해 두면 편합니다.- PKCE를 쓰면 더 안전합니다(4절의 PKCE 값 만들기). 서버가 시크릿을 보관하는 위 예제에서는 선택 사항입니다.
상황별 처리
| 상황 | 서비스가 할 일 |
|---|---|
| 사용자가 승인 화면에서 취소했다 | 콜백에 error=access_denied가 옵니다. 로그인 화면으로 돌려보내면 됩니다. |
| 두 번째 로그인부터 | 승인 화면 없이 바로 코드가 옵니다. 콜백 처리는 첫 로그인과 같고, 사용자 ID로 기존 회원이 찾아집니다. |
| 사용자가 AI Market 마이페이지에서 연결을 끊었다 | 다음 로그인 때 승인 화면이 다시 뜨고 같은 사용자 ID가 돌아오므로 회원은 그대로 유지됩니다. 끊김을 미리 알고 싶으면 9절의 연결 끊김 확인을 주기적으로 호출하세요. |
| 회원이 서비스를 탈퇴했다 | 서비스의 회원 정보를 지우고 연결 끊기를 호출합니다. 같은 사용자가 다시 가입하면 새 회원으로 처리하면 됩니다. |
| 구독 확인과 사용량 보고까지 | 승인 요청에 service_id를 더하면 승인 화면에 구독·사용량 항목이 함께 표시되고, 이후 저장한 사용자 ID로 7절·8절을 호출합니다. |
| 테스트 | 샌드박스의 서비스 선택에서 '서비스 없이 로그인만'을 고르면 승인 → 토큰 → 사용자 정보까지 눌러 볼 수 있습니다. 테스트 키의 승인은 크리에이터 본인 계정만 통과하므로, 다른 사용자의 가입은 운영 키로 확인합니다. |
연동 방식 고르기
AI Market 계정은 서비스에 두 가지 방식으로 연동할 수 있습니다. 두 방식은 같은 승인 화면과 같은 API를 사용하며, 서비스에 맞는 쪽을 고르거나 둘 다 제공할 수 있습니다.
어느 방식이든 사용자가 동의하면 사용자와 서비스가 연결되고, 그 뒤 구독 확인과 사용량 보고를 호출할 수 있습니다.
| 방식 | 어떤 서비스에 | 시작 위치 | 승인 요청 | 돌아온 뒤 할 일 |
|---|---|---|---|---|
| ① AI Market 로그인 (간편 로그인·회원가입) | 회원 체계를 AI Market 계정에 맡기거나, 가입 절차 없이 바로 쓰게 하려는 서비스 | 서비스의 로그인 화면 [AI Market 계정으로 로그인] | mode 생략 | 사용자 ID로 회원을 찾아 로그인시키고, 처음 보는 사용자 ID이면 가입시킵니다(처음부터 끝까지는 위 절) |
| ② 계정 연결 (기존 회원의 SNS 계정 연동) | 자체 회원이 이미 있고, 그 회원이 AI Market에서 구매한 구독을 쓰게 하려는 서비스 | 서비스에 로그인한 회원의 설정 화면 [AI Market 계정 연결] | mode=link | 돌아온 사용자 ID를 지금 로그인한 회원에 저장합니다 |
- 구독 확인과 사용량 보고를 쓰려면 승인 요청에
service_id를 포함합니다. 로그인만 필요하면 생략합니다. - 한 서비스가 두 방식을 모두 제공해도 됩니다. 사용자와 서비스의 연결은 하나이므로, ①로 가입한 회원이 ②를 다시 할 필요는 없습니다.
- 어느 방식이든 승인 화면에는 지금 로그인한 AI Market 계정이 표시되고, 사용자는 [다른 계정으로 진행]으로 계정을 바꿀 수 있습니다.
AI Market 로그인
카카오 로그인, 네이버 로그인과 같은 방식으로 서비스에 ‘AI Market 계정으로 로그인’을 추가할 수 있습니다.
사용자는 서비스에 따로 가입하지 않고 AI Market 계정으로 가입하고 로그인하며, 서비스는 사용자가 동의한 정보만 제공받습니다.
- 서비스의 로그인 화면에 [AI Market 계정으로 로그인] 버튼을 두고, 선택하면 사용자를 승인 화면으로 이동시킵니다(4절).
- 사용자가 AI Market에 로그인하고 정보 제공에 동의하면 인가 코드가 전달됩니다.
AI Market 계정이 없는 사용자는 로그인 화면에서 회원가입(이메일, 카카오, 네이버, Google)을 마친 후 자동으로 승인 화면으로 돌아옵니다. - 서버에서 인가 코드를 토큰으로 교환하고(5절), 접근 토큰으로 사용자 정보를 조회합니다(6절).
- 응답의
id(사용자 ID)로 서비스의 회원을 찾습니다.
처음 보는 사용자 ID이면 서비스의 회원으로 가입시키고, 이미 저장된 사용자 ID이면 로그인 처리합니다.
동의 항목(scope) | 제공되는 정보 | 응답 필드 |
|---|---|---|
| 항상 제공 | 사용자 ID | id |
profile | 닉네임, 프로필 사진 | nickname, profileImageUrl |
email | 이메일 주소, 이메일 인증 여부 | email, emailVerified |
name | 이름 | name |
- 사용자 ID는 AI Market 계정마다 고정된 값(UUID)이며 변경되지 않습니다.
이메일과 닉네임은 변경될 수 있으므로 서비스의 회원은 사용자 ID를 기준으로 구분하세요. 승인 화면에서는 ‘회원 번호’로 안내됩니다. - 동의를 마친 사용자와 서비스는 연결된 상태가 됩니다. 연결된 사용자는 다음 로그인부터 승인 화면 없이 바로 인가 코드가 전달됩니다.
요청한 동의 항목이 늘어난 경우에만 승인 화면이 다시 표시됩니다. - 연결은 서비스가 연결 끊기를 요청하거나(9절), 사용자가 AI Market › 마이페이지 › 소셜 계정 연동의 ‘연결된 서비스’에서 끊을 때까지 유지됩니다.
구독 확인과 사용량 API는 연결된 사용자에 대해서만 호출할 수 있습니다. - 로그인만 연동하는 경우
service_id는 생략할 수 있습니다. 구독 상태와 사용량까지 연동하려면service_id를 포함하세요.
계정 연결
자체 회원이 있는 서비스는 회원이 서비스에 로그인한 상태에서 AI Market 계정을 연결하게 할 수 있습니다. 카카오, 네이버 계정을 설정 화면에서 연동하는 것과 같은 방식입니다.
AI Market에서 구독을 구매한 사용자가 서비스의 기존 회원이라면, 이 연결로 그 회원의 구독을 확인하고 사용량을 보고할 수 있습니다.
- 회원의 설정 화면에 [AI Market 계정 연결] 버튼을 두고, 선택하면 승인 요청에
mode=link를 더해 승인 화면으로 이동시킵니다(4절). 구독을 확인할 서비스가 있으면service_id도 포함합니다. - 승인 화면은 ‘AI Market 계정 연결’ 제목으로 표시됩니다. 동의 항목, 버튼, 연결의 동작은 로그인과 같습니다.
- 콜백으로 받은 인가 코드를 토큰으로 교환하고(5절), 사용자 정보를 조회해(6절) 응답의
id(사용자 ID)를 지금 서비스에 로그인한 회원에 저장합니다. - 이후 그 회원의 구독 확인과 사용량 보고는 저장한 사용자 ID를
userId로 전달해 호출합니다(7절, 8절).
- 연결 전 확인: 돌아온 사용자 ID가 이미 다른 회원에 저장되어 있으면 연결하지 말고 안내하세요. 하나의 AI Market 계정은 서비스의 회원 하나에만 연결하는 것이 안전합니다.
state확인은 필수입니다. 확인하지 않으면 공격자가 자신의 AI Market 계정을 다른 회원에 연결시키는 요청을 꾸밀 수 있습니다(4절의 콜백 처리).- 회원이 서비스에서 연결을 해제하면 서비스에 저장한 사용자 ID를 지우고 연결 끊기를 호출하세요. 회원이 AI Market 쪽에서 끊은 경우는 9절의 연결 끊김 확인으로 알 수 있습니다.
- 회원이 브라우저에 다른 AI Market 계정으로 로그인되어 있을 수 있습니다. 승인 화면이 지금 계정을 보여 주고 [다른 계정으로 진행]을 제공하므로, 서비스는 돌아온 사용자 ID만 믿고 처리하면 됩니다.
버튼 만들기
버튼 문구는 ‘AI Market 계정으로 로그인’(로그인) 또는 ‘AI Market 계정 연결’(계정 연결)을 권장합니다. 색은 AI Market 네이비(#070f54)에 흰 글자, 또는 흰 바탕에 네이비 테두리와 글자를 사용하고, 그라데이션이나 다른 색은 사용하지 마세요.
아래 이미지를 그대로 쓰거나(2배 해상도 PNG, 표시 크기 320×52), CSS로 같은 모양을 만들 수 있습니다.
영문 화면용: Sign in with AI Market · Link AI Market account
<a class="aimarket-btn" href="/auth/aimarket/start"><b>AI Market</b> 계정으로 로그인</a>
.aimarket-btn { display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
min-width: 320px; height: 52px; padding: 0 20px; border-radius: 10px; border: 1px solid #070f54;
background: #070f54; color: #fff; font-size: 16px; font-weight: 700; text-decoration: none; white-space: nowrap; }
.aimarket-btn b { font-weight: 900; }
.aimarket-btn.outline { background: #fff; color: #070f54; } /* 흰 바탕 버전 */
- 버튼을 누르면 서비스의 서버가 승인 요청 주소를 만들어 사용자를 이동시킵니다(4절).
client_secret은 버튼이나 브라우저 코드에 넣지 않습니다. - 높이는 44px 이상으로 유지하고, 다른 소셜 로그인 버튼과 같은 너비로 나란히 두세요.
1. API 키 발급
개발자 센터 › API 키에서 발급합니다. 계정마다 운영 키 한 쌍과 테스트 키 한 쌍이 발급됩니다.
연동할 서비스는 키가 아니라 승인 요청의 service_id로 지정합니다.
| 키 | 형식 | 용도 |
|---|---|---|
| 운영 키 | nxc_… / 시크릿 nxs_… | 실제 구매자의 구독 상태와 남은 보유량을 확인합니다. 판매 중인 서비스(활성 상태)만 승인할 수 있습니다. |
| 테스트 키 | nxc_test_… / 시크릿 nxs_test_… | 서비스 심사 전 연동을 만들고 검증합니다. 크리에이터 본인 계정으로만 승인됩니다. 실제 구독 없이 ‘구독 중’으로 응답하며, 사용량은 테스트 잔액에만 기록됩니다. |
client_id는 공개되어도 되는 식별자입니다.client_secret은 발급 직후 한 번만 표시되므로 서버 설정에 바로 저장하세요.
창을 닫으면 다시 확인할 수 없으며, 분실한 경우 [시크릿 재발급]으로 새 시크릿을 발급받습니다.
재발급 즉시 기존 시크릿은 사용할 수 없으며,client_id와 사용자 토큰은 유지됩니다.client_id와client_secret은 서버 API의 API 키로 사용합니다.client_id:client_secret을 Base64로 인코딩해Authorization: Basic헤더로 전달합니다(7절).- [비활성화]는 승인, 토큰 발급, 구독 확인을 일시 정지하는 기능입니다. 다시 활성화하면 기존 연동을 그대로 사용할 수 있습니다.
시크릿이나 토큰이 유출된 경우에는 [시크릿 재발급]과 [연동 전체 해제](발급된 모든 사용자 토큰을 영구 폐기)를 함께 실행하세요.
2. 리다이렉트 URI 등록
승인 후 사용자를 돌려보낼 리다이렉트 URI를 API 키마다 등록합니다.
API 키 화면의 각 키 카드에서 한 줄에 하나씩, 최대 10개까지 입력할 수 있습니다.
- 요청의
redirect_uri는 등록한 값과 정확히 일치해야 합니다. 프로토콜, 포트, 슬래시, 대소문자까지 모두 일치해야 합니다. http://또는https://로 시작하는 절대 주소만 등록할 수 있으며, 프래그먼트(#)는 사용할 수 없습니다. 개발 단계인 경우에는http://localhost:3000/callback과 같은 로컬 주소도 등록할 수 있습니다.- 샌드박스에서 사용하는 콜백 주소 (
https://ai-market.co.kr/developer/sandbox/callback)는
승인 화면을 처음 열 때 테스트 키에 자동으로 등록됩니다.
3. 연동할 서비스 ID
API 키 화면 아래의 ‘연동할 서비스 ID’ 표에서 서비스 ID(UUID)를 복사하여
승인 요청의 service_id와 서버 API의 serviceId 값으로 전달합니다.
- 운영 키는 활성 상태인 서비스만, 테스트 키는 삭제되지 않은 모든 서비스
(임시저장, 검토 중 포함)를 승인할 수 있습니다. - 서비스가 여러 개인 경우에도 사용자와의 연결은 API 키마다 하나입니다.
연결된 사용자는 서버 API의serviceId만 바꿔 본인이 등록한 다른 서비스의 구독도 조회할 수 있습니다. 다른 크리에이터의 서비스는 조회할 수 없습니다. - 구독 확인이 필요한 코드에는
service_id를 포함하세요. 로그인만 연동하는 경우에는 생략합니다.
4. 승인 요청
서비스에서 로그인 또는 구독 확인이 필요한 경우, 사용자를 아래 주소로 이동시킵니다.
로그인 상태가 아닌 경우 로그인 후 자동으로 승인 화면으로 이동합니다. AI Market 계정이 없는 사용자는 회원가입을 마친 후 승인 화면으로 돌아옵니다.
GET https://ai-market.co.kr/connect/authorize
?client_id=nxc_xxxxx
&redirect_uri=https%3A%2F%2Fmyservice.com%2Fcallback
&service_id=68f5f1e7-… # 구독 확인이 필요한 경우
&scope=profile%20email%20name # 동의 항목(선택)
&mode=link # 기존 회원의 계정 연결일 때(선택)
&state=임의의_문자열
&code_challenge=…&code_challenge_method=S256 # PKCE(선택)
| 파라미터 | 필수 | 설명 |
|---|---|---|
client_id | 필수 | 발급받은 클라이언트 ID(운영 키 또는 테스트 키) |
redirect_uri | 필수 | 등록한 리다이렉트 URI와 정확히 일치하는 값(URL 인코딩) |
service_id | 선택 | 연동할 서비스의 ID(3절). 구독 확인과 사용량 API를 사용할 서비스를 지정합니다. 생략하면 로그인만 연동합니다 |
scope | 선택 | 제공받을 정보. profile, email, name을 공백으로 구분해 전달합니다. 생략하면 세 항목을 모두 요청합니다 |
state | 권장 | CSRF 방지용 임의 값. 콜백에 그대로 돌아오므로 보낸 값과 같은지 확인합니다 |
prompt | 선택 | consent를 전달하면 이미 동의한 사용자에게도 승인 화면을 다시 표시합니다 |
mode | 선택 | 승인 화면의 용도 문구. login(기본, ‘AI Market 계정으로 로그인’) 또는 link(‘AI Market 계정 연결’ — 기존 회원의 계정 연결). 동의 항목과 결과는 같습니다 |
code_challenge | 선택 | PKCE 챌린지(S256). base64url 43~128자. 보냈다면 5절에서 code_verifier가 필수입니다 |
code_challenge_method | 선택 | S256만 지원합니다. plain은 거절됩니다 |
PKCE 값 만들기
Node.js 예제: verifier, challenge, state 생성과 리다이렉트
import crypto from 'node:crypto';
const verifier = crypto.randomBytes(48).toString('base64url'); // 64자, 세션에 보관
const challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
const state = crypto.randomBytes(18).toString('base64url'); // 세션에 보관
const url = new URL('https://ai-market.co.kr/connect/authorize');
url.search = new URLSearchParams({
client_id: process.env.NEXA_CLIENT_ID,
redirect_uri: 'https://myservice.com/callback',
service_id: process.env.NEXA_SERVICE_ID,
scope: 'profile email name',
state,
code_challenge: challenge,
code_challenge_method: 'S256',
}).toString();
res.redirect(url.toString());
콜백 처리
사용자가 승인하면 등록한 리다이렉트 URI로 code와 state가 전달됩니다.
거절하거나 취소한 경우 error=access_denied가 전달됩니다.
https://myservice.com/callback?code=nxac_xxxxx&state=보낸_값 # 승인
https://myservice.com/callback?error=access_denied&error_description=User+denied+access&state=보낸_값 # 취소
- 콜백으로 전달된
state가 세션에 저장한 값과 일치하지 않는 경우 요청 처리를 중단하세요. - 인가 코드는 10분 동안 유효하며 한 번만 사용할 수 있습니다.
발급받은 인가 코드를 사용해 서버에서 토큰을 발급받으세요. - 이미 동의한 사용자는 승인 화면이 표시되지 않고 바로
code가 전달됩니다. 콜백 처리는 동일합니다.
client_id, redirect_uri, service_id, scope가 유효하지 않은 경우 오류 화면이 표시되며 리다이렉트는 진행되지 않습니다.
콜백이 전달되지 않는 경우 redirect_uri가 등록된 값과 정확히 일치하는지, service_id가 올바른 서비스의 ID인지 확인하세요. 운영 키는 활성 상태인 서비스에만 사용할 수 있습니다.
오류 코드는 API 레퍼런스에서 확인할 수 있습니다.
5. 토큰 교환
인가 코드를 사용해 접근 토큰을 발급받습니다. client_secret이 필요하므로 요청은 반드시 서버에서 전송해야 합니다.
요청 본문은 application/x-www-form-urlencoded 형식입니다.
curl -X POST https://ai-market.co.kr/api/v1/connect/token \
-d grant_type=authorization_code \
-d code=nxac_xxxxx \
-d client_id=nxc_xxxxx \
-d client_secret=nxs_xxxxx \
-d redirect_uri=https://myservice.com/callback \
-d code_verifier=… # PKCE를 사용한 경우에만
{
"access_token": "nxa_...",
"token_type": "Bearer",
"expires_in": 2592000,
"refresh_token": "nxr_...",
"refresh_token_expires_in": 15552000,
"scope": "profile email name"
}
redirect_uri는 승인 요청에 전달한 값과 정확히 일치해야 합니다.- 접근 토큰은 30일, 갱신 토큰은 180일 동안 유효합니다.
토큰은 연동한 서비스와 승인한 사용자 기준으로 구분되므로 사용자별로 안전하게 보관하세요. - 접근 토큰은 로그인한 사용자를 확인하는 데 사용합니다(6절).
구독 확인과 사용량 API는 API 키와 사용자 ID로 호출하므로(7절, 8절), 사용자 ID를 저장한 뒤에는 토큰을 보관하지 않아도 됩니다. - 요청에 실패한 경우
{"error", "error_description"}형식으로 오류가 반환됩니다.invalid_grant는 인가 코드 만료·재사용,redirect_uri불일치 또는 PKCE 검증 실패 시 발생할 수 있습니다. 해당 오류가 발생한 경우 승인 요청 단계부터 다시 시도하세요.
Node.js 예제: 콜백 라우트에서 토큰 교환과 로그인 처리
app.get('/callback', async (req, res) => {
const { code, state, error } = req.query;
if (error || state !== req.session.nexaState) return res.status(400).send('approval failed');
const r = await fetch('https://ai-market.co.kr/api/v1/connect/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code,
client_id: process.env.NEXA_CLIENT_ID,
client_secret: process.env.NEXA_CLIENT_SECRET,
redirect_uri: 'https://myservice.com/callback',
code_verifier: req.session.nexaVerifier,
}),
});
if (!r.ok) return res.status(400).json(await r.json()); // { error, error_description }
const token = await r.json(); // access_token · refresh_token · expires_in · scope
const me = await getMe(token.access_token); // 6절 — { id, email, name, nickname, … }
const user = await findOrCreateUserByNexaId(me); // me.id 로 회원을 찾고, 없으면 가입
req.session.userId = user.id;
res.redirect('/');
});
Python 예제: requests
import requests
r = requests.post('https://ai-market.co.kr/api/v1/connect/token', data={
'grant_type': 'authorization_code',
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': 'https://myservice.com/callback',
'code_verifier': verifier, # PKCE를 사용한 경우에만
}, timeout=10)
r.raise_for_status()
token = r.json() # access_token, refresh_token, expires_in
6. 사용자 정보 조회
로그인한 사용자가 누구인지 확인합니다. 토큰 교환 직후 접근 토큰으로 호출하며, 응답의 id가 사용자 ID입니다.
사용자 ID를 서비스의 회원 정보와 함께 저장하세요. 이후 구독 확인과 사용량 API에서 userId로 사용합니다.
curl https://ai-market.co.kr/api/v1/connect/me \
-H "Authorization: Bearer nxa_..."
{
"id": "22270e30-...",
"mode": "live",
"scope": "profile email name",
"email": "user@example.com",
"emailVerified": true,
"name": "홍길동",
"nickname": "gildong",
"profileImageUrl": "https://ai-market.co.kr/nexa/avatars/...",
"connectedAt": "2026-10-06T10:12:44+09:00"
}
id는 항상 반환됩니다. 사용자가 동의하지 않은 항목은null로 반환됩니다.- 처음 보는 사용자 ID이면 서비스의 회원으로 가입시키고, 이미 저장된 사용자 ID이면 로그인 처리합니다.
이메일은 사용자가 변경할 수 있으므로 회원을 구분하는 기준으로 사용하지 마세요. - 저장해 둔 사용자 ID로 최신 정보를 다시 조회할 수도 있습니다. 이 경우 접근 토큰 대신 API 키와
userId를 사용합니다(7절의 인증 방식과 동일). - 요청 한도는 분당 300회입니다.
curl "https://ai-market.co.kr/api/v1/connect/me?userId=22270e30-..." \
-u nxc_xxxxx:nxs_xxxxx # API 키로 조회
Node.js 예제: 사용자 정보 조회와 회원 가입·로그인 처리
async function getMe(accessToken) {
const r = await fetch('https://ai-market.co.kr/api/v1/connect/me', { headers: { Authorization: `Bearer ${accessToken}` } });
if (!r.ok) throw new Error((await r.json()).error);
return r.json();
}
async function findOrCreateUserByNexaId(me) {
let user = await db.users.findOne({ nexaUserId: me.id }); // 사용자 ID 로 회원 조회
if (!user) {
user = await db.users.insert({ // 처음 로그인한 사용자 = 회원가입
nexaUserId: me.id, email: me.email, name: me.name, nickname: me.nickname, avatar: me.profileImageUrl,
});
}
return user;
}
7. 구독 확인
서버에서 API 키와 사용자 ID로 해당 사용자의 구독 상태를 조회합니다.userId에는 6절에서 저장한 사용자 ID를, serviceId에는 연동할 서비스 ID(3절)를 전달합니다. 연결된 사용자만 조회할 수 있습니다.
curl "https://ai-market.co.kr/api/v1/connect/subscription?userId=22270e30-...&serviceId=68f5f1e7-..." \
-u nxc_xxxxx:nxs_xxxxx
| 항목 | 필수 | 설명 |
|---|---|---|
| API 키 | 필수 | Authorization: Basic base64(client_id:client_secret) 헤더. curl의 -u client_id:client_secret이 이 헤더를 전송합니다 |
userId | 필수 | 구독을 확인할 사용자의 ID(6절 응답의 id) |
serviceId | 필수 | 구독을 확인할 서비스의 ID(3절). 본인이 등록한 서비스만 지정할 수 있습니다 |
{
"active": true,
"status": "ACTIVE",
"mode": "live",
"user": { "id": "22270e30-...", "email": "user@example.com", "name": "홍길동", "nickname": "gildong" },
"service": { "id": "68f5f1e7-...", "title": "내 AI 서비스" },
"subscription": {
"id": "741171b0-...", "planId": "40413c13-...", "planName": "프로 플랜", "priceKrw": 29000,
"startedAt": "2026-08-08T02:31:12+09:00", "nextBillingAt": "2026-09-08T02:31:12+09:00",
"cancelledAt": null, "graceUntil": null
},
"usage": null,
"checkedAt": "2026-08-08T03:02:44+09:00"
}
active 값만으로 기본적인 구독 상태를 판단할 수 있습니다.
상태별 처리가 필요한 경우 아래 5가지 status 값을 참고하세요.
| status | active | 의미 | 권장 처리 |
|---|---|---|---|
ACTIVE | true | 정상 구독 중 | 정상 이용 |
PAST_DUE | true | 결제 실패 후 유예 중graceUntil까지 결제 재시도 | 이용 허용, 결제 수단 확인 안내 |
CANCEL_SCHEDULED | true | 해지 신청됨. nextBillingAt까지 유효 | 이용 허용, 종료 예정일 안내 |
EXPIRED | false | 과거 구독은 있으나 지금은 유효하지 않음 | 차단, 재구독 안내 ( subscription에 마지막 구독 정보) |
NONE | false | 이 서비스를 구독한 이력 없음 | 차단, 구독 안내 |
- 구독 상태는 요청 시점에 실시간으로 판정되며, 해지와 연체 상태도 즉시 반영됩니다.
로그인 시점에 확인하고, 세션 유지 중에는 주기적으로(예: 1시간마다) 재확인하는 것을 권장합니다. - 요청 한도는 분당 300회입니다. 구독 상태 조회 결과는 일정 시간 캐시하여 사용할 수 있습니다.
해지 등 상태 변경이 발생할 수 있으므로 캐시된 결과는 주기적으로 갱신하세요. - 시간은 ISO 8601 형식이며 KST 오프셋(
+09:00)을 포함합니다.nextBillingAt은ACTIVE상태에서 다음 결제일을,CANCEL_SCHEDULED상태에서 이용 종료일을 의미합니다. 구독은 월 단위입니다. - [단건 판매] 서비스는 항상
NONE으로 응답합니다.
구독 확인은 구독형(월 결제) 서비스에만 적용됩니다. - 연결되지 않은 사용자는
403 user_not_connected로 응답합니다. 승인한 적이 없거나 연결을 끊은 사용자, 정지 또는 탈퇴한 계정이 해당됩니다.
이 경우 사용자에게 다시 로그인하도록 안내하세요(4절).
접근 토큰(Authorization: Bearer nxa_…)으로도 구독 확인과 사용량 API를 호출할 수 있습니다. 이 경우 토큰의 사용자와 서비스가 대상이 되므로 userId와 serviceId는 생략합니다.
2026년 10월 이전에 접근 토큰으로 연동한 코드는 수정 없이 동작합니다.
Node.js 예제: API 키로 구독 확인
const apiKey = 'Basic ' + Buffer.from(`${process.env.NEXA_CLIENT_ID}:${process.env.NEXA_CLIENT_SECRET}`).toString('base64');
async function checkSubscription(nexaUserId) {
const q = new URLSearchParams({ userId: nexaUserId, serviceId: process.env.NEXA_SERVICE_ID });
const r = await fetch(`https://ai-market.co.kr/api/v1/connect/subscription?${q}`, { headers: { Authorization: apiKey } });
if (!r.ok) {
const err = await r.json(); // { error, error_description }
return { active: false, relogin: err.error === 'user_not_connected' }; // 연결이 끊긴 사용자는 4절부터 다시
}
const sub = await r.json();
return { active: sub.active, status: sub.status, usage: sub.usage };
}
8. 사용량 조회와 보고
월 제공량(토큰·크레딧)을 설정한 구독형 서비스는 어느 사용자가 얼마를 사용했는지를 서버에서 전송하면 AI Market에서 해당 사용자의 잔액이 차감됩니다.
구독 확인과 동일하게 API 키와 userId, serviceId를 사용하며, 월 제공량을 설정하지 않은 서비스는 400 usage_not_enabled로 응답합니다.
조회
curl "https://ai-market.co.kr/api/v1/connect/usage?userId=22270e30-...&serviceId=68f5f1e7-..." \
-u nxc_xxxxx:nxs_xxxxx
보고
userId는 사용량을 차감할 사용자입니다. amount는 이번 요청의 사용량이며, 1 이상의 정수여야 합니다. 중복 요청 가능성이 있는 경우 idempotencyKey(100자 이하)를 포함하세요. 동일한 키는 중복 차감 없이 현재 잔액만 반환합니다(report.duplicate: true).
curl -X POST https://ai-market.co.kr/api/v1/connect/usage \
-u nxc_xxxxx:nxs_xxxxx -H "Content-Type: application/json" \
-d '{"userId": "22270e30-...", "serviceId": "68f5f1e7-...", "amount": 150, "idempotencyKey": "req-20260922-0001", "memo": "chat"}'
{
"active": true, "status": "ACTIVE", "mode": "live",
"user": { "id": "22270e30-..." },
"service": { "id": "68f5f1e7-..." },
"subscription": { "id": "741171b0-...", "planId": "40413c13-...", "planName": "프로 플랜", "nextBillingAt": "2026-10-22T19:20:11+09:00" },
"report": { "amount": 150, "fromAllowance": 150, "fromTopup": 0, "overage": 0, "duplicate": false, "recordedAt": "2026-09-22T19:30:02+09:00" },
"usage": {
"unit": "크레딧", "monthlyTokens": 1000,
"period": { "start": "2026-09-22T19:20:11+09:00", "end": "2026-10-22T19:20:11+09:00", "used": 150 },
"totalTokens": 2000, "totalUsed": 150, "remaining": 1850,
"allowanceRemaining": 850, "topupRemaining": 1000, "lastReportedAt": "2026-09-22T19:30:02+09:00"
},
"checkedAt": "2026-09-22T19:30:02+09:00"
}
| 필드 | 의미 |
|---|---|
user.id | 사용량이 차감된 사용자의 ID. 요청의 userId와 같은 값 |
totalUsed | 해당 사용자의 누적 사용량. 이 사용자의 구독에서 현재까지 집계된 사용량 |
totalTokens | 해당 사용자의 전체 보유량. 이번 결제 기간의 사용 가능량(월 제공량 + 이월된 추가 구매분 + 기간 중 추가 구매분) |
remaining | 해당 사용자의 남은 보유량. 월 제공 잔여량(allowanceRemaining) + 추가 구매 잔여량(topupRemaining) |
period | 이번 결제 기간과 해당 기간의 사용량. 월 제공량은 결제일마다 갱신되며 이월되지 않고, 추가 구매분만 다음 결제 기간으로 이월됩니다. |
- 잔액은 사용자의 구독마다 따로 관리됩니다. 한 사용자의 사용량 보고는 다른 사용자의 잔액에 영향을 주지 않습니다.
- 차감 순서는 월 제공량 → 추가 구매분입니다. 잔여량을 초과하여 전송된 사용량도 처리되지만, 잔여량은 0으로 유지되며 초과분은
report.overage로 반환됩니다.
잔여량이 0인 경우 서비스 이용 여부는 각 서비스의 정책에 따라 처리합니다. - 추가 구매(팩)는 구매자 계정의 AI Market › 마이페이지 › 내 구독에서 구매할 수 있습니다.
잔여량이 부족한 경우 해당 화면으로 안내하세요. - 월 제공량과 단위는 구독 시점을 기준으로 고정됩니다.
이후 플랜의 제공량을 변경해도 기존 구독에는 반영되지 않습니다. - 폼 인코딩(
userId=...&serviceId=...&amount=150&idempotencyKey=...) 형식도 지원합니다.
요청 한도는 조회와 보고 각각 분당 300회입니다.
Node.js 예제: 사용자의 사용량 보고
async function reportUsage(nexaUserId, amount, requestId) {
const r = await fetch('https://ai-market.co.kr/api/v1/connect/usage', {
method: 'POST',
headers: { Authorization: apiKey, 'Content-Type': 'application/json' }, // apiKey 는 7절 예제와 같은 값
body: JSON.stringify({ userId: nexaUserId, serviceId: process.env.NEXA_SERVICE_ID, amount, idempotencyKey: requestId }),
});
const body = await r.json();
if (!r.ok) throw new Error(body.error); // user_not_connected · subscription_inactive · usage_not_enabled …
return body.usage.remaining; // 이 사용자의 남은 보유량
}
9. 토큰 갱신·폐기와 연결 끊기
갱신: 접근 토큰이 만료되어 401 invalid_token이 반환된 경우 갱신 토큰을 사용해 새 접근 토큰과 갱신 토큰을 발급받습니다.
갱신 시 기존 접근 토큰과 갱신 토큰은 즉시 무효화되므로 새로 발급된 토큰을 저장하세요.
curl -X POST https://ai-market.co.kr/api/v1/connect/token \
-d grant_type=refresh_token \
-d refresh_token=nxr_... \
-d client_id=nxc_xxxxx \
-d client_secret=nxs_xxxxx
폐기: 사용자가 서비스에서 로그아웃하거나 연결을 해제하는 경우 토큰을 폐기합니다. 접근 토큰 또는 갱신 토큰 중 하나만 전달하면 두 토큰이 모두 폐기됩니다.
이미 폐기되었거나 확인할 수 없는 토큰을 전달해도 200 OK로 응답합니다.
curl -X POST https://ai-market.co.kr/api/v1/connect/revoke \
-d token=nxa_... \
-d client_id=nxc_xxxxx \
-d client_secret=nxs_xxxxx
연결 끊기: 사용자가 서비스에서 탈퇴하거나 AI Market 계정 연결 해제를 요청한 경우 연결을 끊습니다. 해당 사용자에게 발급된 토큰이 모두 폐기되며, 이후 이 사용자에 대한 서버 API 호출은 403 user_not_connected로 응답합니다.
사용자가 다시 로그인하면 승인 화면에서 동의를 다시 받습니다.
curl -X POST https://ai-market.co.kr/api/v1/connect/unlink \
-u nxc_xxxxx:nxs_xxxxx -H "Content-Type: application/json" \
-d '{"userId": "22270e30-..."}'
{ "id": "22270e30-..." } # 200 OK — 연결을 끊은 사용자 ID
- 토큰 폐기는 토큰만 무효화하며 연결은 유지됩니다. 연결이 유지되는 동안에는 API 키와 사용자 ID로 구독 확인과 사용량 API를 계속 호출할 수 있습니다.
- 사용자는 AI Market › 마이페이지 › 소셜 계정 연동의 ‘연결된 서비스’에서 직접 연결을 끊을 수 있습니다.
이 경우에도 서버 API는403 user_not_connected로 응답하므로, 해당 응답을 받으면 서비스에서도 연결 해제로 처리하세요.
연결 끊김 확인: 사용자가 AI Market에서 직접 연결을 끊었거나 탈퇴한 경우를 서버 API의 403 응답 전에 알고 싶다면, 끊긴 연결 목록을 주기적으로 조회하세요. API 키로 호출하며 since 이후에 끊긴 연결을 시각 순으로 돌려줍니다.
응답의 next를 저장해 두었다가 다음 조회의 since로 전달하면 빠짐없이 이어서 받을 수 있습니다. 10분 정도의 주기면 충분합니다.
curl "https://ai-market.co.kr/api/v1/connect/disconnections?since=2026-10-06T09:00:00%2B09:00" \
-u nxc_xxxxx:nxs_xxxxx
{ "mode": "live", "since": "2026-10-06T09:00:00+09:00",
"items": [ { "userId": "22270e30-...", "disconnectedAt": "2026-10-06T10:12:44.183+09:00", "by": "user" } ],
"hasMore": false, "next": "2026-10-06T10:12:44.183+09:00" }
by는 끊은 주체입니다.user(사용자가 마이페이지에서 끊음),withdrawal(사용자 탈퇴),service(서비스가 연결 끊기 API로 끊음 — 보통 건너뜁니다).- 다시 연결된 사용자는 목록에서 빠집니다. 목록에 있는 사용자 ID는 서비스에서도 연결 해제로 처리하세요.
10. 테스트 키와 샌드박스
검토 중이거나 임시저장 상태인 서비스도 테스트 키를 사용해 전체 흐름(승인 → 토큰 발급 → 사용자 정보 조회 → 구독 확인 → 사용량 처리)을 테스트할 수 있습니다.
- 승인 화면은 키를 발급한 크리에이터 계정으로 로그인한 경우에만 사용할 수 있습니다.
다른 계정으로 로그인한 경우test_owner_only오류 화면이 표시됩니다. - 승인 화면에는 ‘테스트 연동’ 표시가 추가되며, 이후 응답에는
"mode": "test"가 포함됩니다.
테스트 토큰은nxa_test_…/nxr_test_…형식으로 발급되어 로그에서 운영 토큰과 구분할 수 있습니다. - 구독 확인은 항상
active: true,status: "ACTIVE"로 응답합니다.
플랜 정보는 해당 서비스의 첫 번째 플랜을 기준으로 하며,nextBillingAt은 승인 시점으로부터 1개월 뒤로 설정됩니다. - 사용량은 테스트 잔액에만 반영됩니다. 월 제공량은 첫 번째 플랜의 설정을 따르며, 설정되지 않은 경우
usage_not_enabled로 응답합니다.
테스트 모드에서는 추가 구매 잔액이 제공되지 않으며,topupRemaining은 0으로 반환됩니다. 승인 시점으로부터 1개월 후 다음 기간이 자동으로 시작됩니다. 초기 상태부터 다시 테스트하려면 [테스트 데이터 초기화]를 실행하세요. - 테스트 키에는 크리에이터 본인 계정만 연결할 수 있으므로, 서버 API의
userId에는 본인의 사용자 ID만 사용할 수 있습니다.
샌드박스 사용법
샌드박스에서는 전체 연동 흐름을 별도의 코드 작성 없이 브라우저에서 테스트할 수 있습니다.
- 테스트 키가 없는 경우 샌드박스에서 바로 발급할 수 있습니다.
client_secret은 발급 시 한 번만 표시되며, 샌드박스 이용에는 필요하지 않습니다. - 연동할 서비스를 선택하면 승인 요청 URL이 실시간으로 생성됩니다.
생성된 URL을 복사하여 작성한 코드와 비교할 수 있습니다. 서비스 없이 로그인만 테스트할 수도 있습니다. - [승인 화면 열기]를 선택하면 실제 승인 화면이 팝업으로 표시됩니다.
승인 완료 후 인가 코드가 샌드박스로 전달되며, 콜백 주소는 테스트 키에 자동으로 등록됩니다. 이미 동의한 경우 승인 화면 없이 인가 코드가 바로 전달됩니다. - 인가 코드가 전달된 후 [토큰 발급]을 선택하고, [사용자 정보 조회]로 사용자 ID를 확인합니다.
이후 [구독 확인]과 사용량 조회·보고를 실행합니다. - [사용자 정보 조회]는 브라우저에서 접근 토큰을 사용해 실제 공개 API로 요청을 전송합니다. 클라이언트 시크릿이 필요한 [구독 확인], [사용량 조회], [사용량 보고], [연결 끊기], [토큰 갱신], [토큰 폐기]는 로그인한 계정 권한으로 샌드박스가 대신 전송합니다.
오른쪽 콘솔에서 상태 코드, 응답 JSON,curl을 확인할 수 있습니다.
11. 운영 전환 체크리스트
-
client_id와client_secret을 운영 키로 교체하고, 응답의mode가live인지 확인합니다.
테스트 키로는 실제 구매자를 검증할 수 없습니다. - 연동할 서비스는 판매 중(활성) 상태여야 합니다. 운영 키는 활성 서비스만 승인할 수 있습니다.
- 운영 도메인의
redirect_uri를 운영 키에 등록했으며, 등록된 값과 요청 값이 정확히 일치합니다. -
client_secret은 서버의 환경 변수 또는 보안 저장소에만 보관합니다. 브라우저, 모바일 앱, 공개 저장소에는 포함하지 않습니다. -
state를 생성하여 요청에 포함하고, 콜백으로 반환된 값이 요청 시 전달한 값과 일치하는지 확인합니다. - 사용자 정보 조회로 받은 사용자 ID(
id)를 서비스의 회원 정보와 함께 저장하고, 서버 API의userId로 사용합니다. - 구독 확인 결과는 일정 시간 동안 임시 저장해 사용합니다.
403 user_not_connected응답 시 사용자에게 다시 로그인을 요청합니다. - 월 제공량이 있는 서비스는 사용 시점마다 사용자 ID와 사용량을 서버에서 전송하며,
remaining값을 기준으로 이용 가능 여부를 판단합니다. - 사용자가 로그아웃한 경우 토큰을 폐기하고, 탈퇴하거나 연결 해제를 선택한 경우 연결을 끊습니다.
12. 문제 해결
| 증상 | 원인과 조치 |
|---|---|
| 콜백이 전달되지 않고 AI Market 오류 화면이 표시됨 | redirect_uri 불일치(invalid_redirect), service_id 형식 오류·서비스 불일치·비활성 서비스(invalid_service), 지원하지 않는 동의 항목(invalid_scope), 비활성 키(invalid_client)를 확인하세요.오류 코드는 API 레퍼런스에서 확인할 수 있습니다. |
테스트 키인데 test_owner_only가 표시된다 | 테스트 키 연동은 키를 발급한 크리에이터 본인 계정으로만 승인할 수 있습니다. 다른 계정(구매자 테스트 계정 등)으로 로그인한 상태인지 확인하세요. |
invalid_grant | 인가 코드 만료(10분) 또는 재사용, redirect_uri 불일치, PKCE 검증 실패, 갱신 토큰 만료 또는 이미 갱신된 경우 발생합니다.해당 오류가 발생한 경우 승인 요청 단계부터 다시 시도하세요. |
invalid_client(401) | client_id와 client_secret이 일치하지 않거나 키가 비활성 상태인 경우 발생합니다.시크릿을 재발급한 경우 기존 시크릿은 즉시 무효화됩니다. 보안상 정확한 원인은 구분하여 제공하지 않습니다. |
invalid_token(401) | 접근 토큰이 만료·폐기·갱신되었거나, 키가 비활성 상태이거나, 사용자 계정이 정지 또는 탈퇴한 경우 발생합니다. 토큰 갱신을 요청하고, 실패한 경우 재승인을 요청하세요. |
user_not_connected(403) | API 키로 지정한 사용자가 연결되어 있지 않은 경우 발생합니다. 승인한 적이 없거나 연결을 끊은 사용자, 정지 또는 탈퇴한 계정이 해당됩니다. 사용자에게 다시 로그인하도록 안내하세요. 보안상 정확한 원인은 구분하여 제공하지 않습니다. |
invalid_service(403) | serviceId가 본인이 등록한 서비스가 아니거나 삭제된 서비스인 경우 발생합니다.API 키 화면의 ‘연동할 서비스 ID’ 표에서 서비스 ID를 확인하세요. |
invalid_request(400) | userId 또는 serviceId가 누락되었거나 UUID 형식이 아닌 경우, amount가 올바르지 않은 경우 발생합니다.error_description에서 원인을 확인하세요. |
usage_not_enabled(400) | 이 서비스에는 월 제공량이 설정되어 있지 않습니다. 서비스 등록 폼의 토큰 항목에서 제공량을 설정하세요. 테스트 키는 첫 번째 플랜을 기준으로 합니다. |
subscription_inactive(403) | 사용량 API는 유효한 구독이 있는 경우에만 동작합니다. 구독 확인의 status 값을 확인하여 구독 상태에 맞게 안내하세요. |
| 429 | 요청 한도를 초과한 경우 발생합니다(토큰과 연결 끊기 분당 30회, 사용자 정보·구독 확인·사용량 분당 300회). 결과를 일정 시간 임시 저장해 사용하고, 재시도 간격을 충분히 확보하세요. |
temporarily_unavailable(503) | 테스트 키에서만 발생하는 오류입니다. 테스트 잔액 생성에 필요한 서비스 정보를 일시적으로 불러올 수 없는 경우 발생합니다. 잠시 후 다시 요청하세요. |
13. 보안 권장 사항
client_secret은 서버에서만 보관해야 합니다. 프런트엔드 번들, 모바일 앱, 공개 저장소에는 포함하지 않으며, 로그에도 기록하지 않습니다.state는 요청마다 새로 생성하여 세션에 저장하고, 콜백으로 반환된 값이 요청 시 전달한 값과 일치하는지 확인합니다. 가능하면 PKCE(S256)도 함께 사용하세요.- 서버 API의
userId에는 서버에서 확인한 값만 사용하세요.
브라우저가 보낸 사용자 ID를 그대로 전달하면 다른 사용자의 잔액이 차감될 수 있습니다. - 사용자별 토큰은 암호화하여 저장하고, 사용자가 연결을 해제한 경우
/unlink요청으로 연결을 끊으세요. 연결을 끊으면 토큰도 함께 폐기됩니다. - 시크릿이나 토큰 유출이 의심되는 경우 API 키 화면에서 [시크릿 재발급]과 [연동 전체 해제]를 함께 실행하세요.
비활성화만으로는 토큰이 폐기되지 않으므로, 다시 활성화한 경우 기존 토큰을 사용할 수 있습니다. - 구독 상태 판정과 접근 제어는 서버에서 처리해야 합니다.
브라우저에 전달된 판정 결과는 변조될 수 있습니다.



