Store SDK
관리(셀러) API 클라이언트 — 세션 로그인, 토큰, 상품/주문 처리
@shop24/store-sdk는 관리(Store) API의 zod 스키마와 fetch 클라이언트를 제공합니다.
관리 API는 2단계 토큰을 씁니다 — 유저 신원을 나타내는 userToken으로 로그인한 뒤, 특정 스토어(테넌트)의
리소스는 그 스토어로 스코프된 storeToken으로 호출합니다. 브라우저 앱은 userToken에 storeId를 실어
교환 없이 곧바로 테넌트 리소스를 호출할 수도 있습니다.
클라이언트 생성
import { createStoreClient } from "@shop24/store-sdk";
const api = createStoreClient({
baseUrl: "https://shop.api.avarlabs.com/v1",
});요청이 어떻게 인증되는지는 아래 옵션으로 결정됩니다.
session— SDK 세션 매니저를 넘기면userToken(Bearer)·401 자동 갱신·세션 정리가 자동 배선됩니다. 브라우저 앱 권장 경로입니다.userToken— 유저 신원 토큰. 인증·스토어 목록 API에 사용합니다.storeToken— 특정 스토어로 스코프된 토큰. 테넌트 리소스 API에 사용합니다.storeId—userToken과 함께 넘기면X-Store-Id로 실려, 교환 없이 테넌트 리소스를 호출합니다.auth.apiToken— PAT 등 비인터랙티브 토큰을 테넌트 리소스 Bearer로 싣습니다(역할 없음).
토큰 값은 문자열 또는 함수(매 요청마다 재평가)로 전달할 수 있어, 상태 저장소의 getter를 그대로 연결하면 됩니다.
브라우저 로그인 — 세션
createStoreSession이 로그인 시작부터 콜백 처리, 토큰 저장, 무음 갱신까지 담당합니다.
저장 매체는 앱이 소유하고(쿠키·스토리지 등), 저장 키는 STORE_SESSION_KEYS로 고정됩니다.
import { createStoreClient, createStoreSession } from "@shop24/store-sdk";
const baseUrl = "https://shop.api.avarlabs.com/v1";
const session = createStoreSession({
baseUrl,
clientId: "{your_client_id}",
storage: {
get: (key) => cookies.get(key),
set: (key, value) => cookies.set(key, value),
remove: (key) => cookies.remove(key),
},
});
// 1) 로그인 시작 — PKCE 생성·저장 후 authorize로 이동
// 브라우저는 자동 리다이렉트, SSR 환경은 이동할 URL을 반환한다
await session.authorize({
redirectUri: "https://your-app.example.com/auth/callback",
scope: "stores:read offline_access", // offline_access = 무음 갱신용 refresh 토큰
});
// 2) 콜백에서 code·state 처리 — state 대조와 PKCE 검증은 SDK가 수행한다
await session.handleCallback(code, state);
// 3) 세션을 클라이언트에 배선하면 이후 요청 인증이 자동 처리된다
const api = createStoreClient({ baseUrl, session });
const me = await api.userinfo();
const stores = await api.auth.listMyStores();
// [{ storeId: "store_demo", storeName: "나의첫번째몰", storeCode: "mystore", role: "OWNER", status: "ACTIVE" }]회원가입 화면으로 진입하려면 authorize({ ..., screen: "signup" })을 씁니다.
session.subscribe(cb)로 토큰 변경을 구독할 수 있습니다.
테넌트 리소스 호출
로그인한 유저가 상품·주문 등 특정 스토어의 리소스를 호출하는 방법은 두 가지입니다.
교환 없이 storeId를 실어 userToken으로 직접 호출:
const api = createStoreClient({
baseUrl,
session, // userToken 소스
storeId: "{storeId}", // X-Store-Id 헤더로 실린다
});
const products = await api.products.list({ page: 1, size: 20 });또는 userToken을 스토어로 스코프된 storeToken으로 교환:
import { exchangeStoreTokenViaOAuth } from "@shop24/store-sdk";
const store = await exchangeStoreTokenViaOAuth({
baseUrl,
clientId: "{your_client_id}",
userToken: session.accessToken,
storeId: "{storeId}",
});
// store: OAuthTokenResponse & { storeId?, role? }
const scoped = createStoreClient({ baseUrl, storeToken: store.access_token });
const products = await scoped.products.list({ page: 1, size: 20 });storeToken이 테넌트를 결정합니다
테넌트 리소스 경로에는 storeId가 없습니다. 교차 테넌트 접근 시 403 TENANT_MISMATCH, 역할 권한
부족 시 403 INSUFFICIENT_ROLE이 반환됩니다.
서버 간 연동 — client_credentials
서버 간 연동은 API 클라이언트 자격증명(Basic 인증)을 사용합니다. 클라이언트가 스토어에 바인딩되어
있어 storeToken이 곧바로 발급됩니다 — 스토어 목록 조회·교환 단계가 필요 없습니다.
// ⚠ client_secret은 서버 환경에서만 사용하세요 (브라우저 금지)
const token = await api.auth.issueStoreTokenByClientCredentials(
"demo-client",
"demo-secret-key",
"product:r order:r", // 선택 — 최소 권한으로 다운스코프
);
// token: { access_token, token_type: "Bearer", expires_in, scope } (refresh 토큰 없음)
const scoped = createStoreClient({ baseUrl, storeToken: token.access_token });
const products = await scoped.products.list({ page: 1 });개인 액세스 토큰(PAT)
CI·백엔드 스크립트처럼 사람이 개입하지 않는 경로는 PAT를 씁니다. 평문 토큰은 발급 응답에서 단 한 번만 노출되므로 안전하게 보관하세요. PAT는 테넌트에 바인딩되지만 역할이 없어, 역할이 필요한 오퍼레이션은 거부됩니다.
// 발급 — OWNER/ADMIN storeToken 필요
const created = await api.apiTokens.create({ name: "ci-bot", scopes: ["product:r", "order:r"] });
// created.token: "s24_pat_..." — 다시 조회할 수 없다
// 사용 — 테넌트 리소스 Bearer로 주입
const bot = createStoreClient({ baseUrl, auth: { apiToken: created.token } });
await bot.products.list({ page: 1 });저수준 OAuth 헬퍼
세션 매니저를 쓰지 않고 토큰 저장·state 대조를 직접 관리한다면 PKCE 헬퍼를 개별로 씁니다.
세션(createStoreSession)이 이 헬퍼들을 감싼 상위 경로입니다.
import {
buildAuthorizeUrl,
createPkcePair,
exchangeAuthorizationCode,
} from "@shop24/store-sdk";
// 1) 로그인 시작 — PKCE 페어 생성 후 authorize로 이동
const { verifier, challenge } = await createPkcePair();
sessionStorage.setItem("pkce", verifier);
location.href = buildAuthorizeUrl({
baseUrl,
clientId: "{your_client_id}",
redirectUri: "https://your-app.example.com/auth/callback",
state: crypto.randomUUID(), // 콜백에서 직접 대조한다
codeChallenge: challenge,
scope: "stores:read offline_access",
});
// 2) 콜백에서 code → userToken 교환
const tokens = await exchangeAuthorizationCode({
baseUrl,
clientId: "{your_client_id}",
redirectUri: "https://your-app.example.com/auth/callback",
code,
codeVerifier: sessionStorage.getItem("pkce")!,
});refreshUserToken({ baseUrl, clientId, refreshToken })로 무음 갱신을 직접 수행할 수 있습니다.
상품 CRUD
// 생성 — Idempotency-Key로 중복 생성 방지 (마지막 인자)
const product = await api.products.create(
{
name: "베이직 티셔츠",
categoryId: "cat_apparel",
salePrice: 19900, // 10원 단위 (아니면 400 INVALID_PRICE_UNIT)
originalPrice: 25000,
stockQuantity: 100,
deliveryType: "PAID",
deliveryFee: 3000,
options: [
{ name: "화이트/M", additionalPrice: 0, stockQuantity: 50 },
{ name: "화이트/L", additionalPrice: 0, stockQuantity: 50 },
],
},
crypto.randomUUID(), // Idempotency-Key
);
// 조회/부분 수정/삭제
await api.products.get(product.productId);
await api.products.patch(product.productId, { salePrice: 17900 });
await api.products.remove(product.productId);
// 판매 상태 일괄 변경 (부분 성공 응답)
const result = await api.products.bulkUpdateStatus(["prod_1", "prod_2"], "SALE", crypto.randomUUID());
// result: { succeeded: string[], failed: [{ productId?, code, message }] }
// 옵션 재고 — 절대값 또는 증감
await api.products.updateOptionStock(product.productId, "opt_1", { stockQuantity: 30 });
await api.products.updateOptionStock(product.productId, "opt_1", { adjustment: -2 });
// 할인 (RATE: %, AMOUNT: 원)
await api.products.setDiscount(product.productId, {
discountType: "RATE",
discountValue: 10,
startAt: "2026-07-01T00:00:00+09:00",
endAt: "2026-07-31T23:59:59+09:00",
});주문 처리 — 발주확인 → 발송
주문/발송/클레임은 OrderItem 단위로 처리합니다. 변경분 조회는 lastChangedFrom 기준입니다.
// 변경된 주문상품 폴링 (예: 신규 결제 완료 건)
const changed = await api.orderItems.listChanged({
lastChangedFrom: "2026-07-08T00:00:00+09:00",
lastChangedType: "ORDER.PAID",
});
for (const item of changed.contents) {
// 1) 발주 확인 (PAID → CONFIRMED)
await api.orderItems.confirm(item.orderItemId, crypto.randomUUID());
// 2) 발송 처리 (CONFIRMED → DISPATCHED)
await api.orderItems.dispatch(
item.orderItemId,
{
deliveryMethod: "COURIER",
carrierCode: "CJGLS",
trackingNumber: "1234567890",
},
crypto.randomUUID(),
);
}
// 일괄 처리 버전
await api.orderItems.confirmBulk(["oi_1", "oi_2"], crypto.randomUUID());
await api.orderItems.dispatchBulk(
[{ orderItemId: "oi_1", deliveryMethod: "COURIER", carrierCode: "CJGLS", trackingNumber: "111" }],
crypto.randomUUID(),
);
// 클레임 처리
await api.claims.approve("claim_1", undefined, crypto.randomUUID());
await api.claims.reject("claim_2", {
reason: "USED_PRODUCT",
detail: "사용 흔적이 확인되어 반품이 불가합니다",
});Idempotency-Key 사용법
생성/승인/발송처럼 재시도 시 중복 부작용이 생길 수 있는 POST는 Idempotency-Key 헤더를 받습니다.
SDK에서는 해당 메서드의 마지막 인자 idempotencyKey로 전달합니다.
const key = crypto.randomUUID();
// 같은 키로 재호출하면 서버가 최초 응답을 재생한다 (TTL 10분)
await api.orderItems.confirm("oi_001", key);
await api.orderItems.confirm("oi_001", key); // 안전 — 중복 처리 없음지원 메서드: products.create, products.bulkUpdateStatus, categories.create,
orderItems.confirm, orderItems.confirmBulk, orderItems.cancelBySeller, orderItems.dispatch,
orderItems.dispatchBulk, claims.approve
에러 처리
Storefront SDK와 동일하게 HTTP 에러는 ApiError(status/code/message/traceId)로 던져집니다
(표준 엔벨로프의 $.error가 매핑됩니다. 성공 응답의 $.data 언랩도 SDK가 처리합니다).
import { ApiError } from "@shop24/store-sdk";
try {
await api.orderItems.confirm("oi_x");
} catch (error) {
if (error instanceof ApiError && error.code === "TENANT_MISMATCH") {
// storeToken의 테넌트와 리소스가 불일치
}
}