Shoplive API 시작 가이드
Shoplive External API v2 를 세 가지 주제로 익힙니다. 인증과 규약을 한 번 익히면, 첫 방송 만들기 → 상품 카탈로그 동기화 → 방송 결과 수집 순서로 실제 운영에 필요한 연동을 완성할 수 있습니다.
개요 & 요구사항
External API v2 는 고객사 시스템(어드민, 백오피스, 커머스 서버)에서 Shoplive 의 방송·상품·셀러·브랜드·숏폼·통계를 서버 대 서버로 다루기 위한 REST API 입니다. Shoplive Admin 화면에서 하는 일의 대부분을 API 로 자동화할 수 있습니다.
요구사항
- accessKey / secretKey — 고객사 계정마다 발급되는 API 키 한 쌍입니다. Shoplive Admin 에서 확인하거나 담당 매니저에게 요청하세요.
secretKey는 서버에만 보관하고 브라우저·앱에 넣지 않습니다. - HTTPS 요청을 보낼 수 있는 서버 환경 — 예제는 curl 기준이며, Node.js · Python · Java · Go · Ruby 토큰 생성 예제를 함께 제공합니다.
- External API v2 사용 설정 — 계정에 API v2 사용이 켜져 있어야 합니다. 담당 매니저에게 확인하세요.
- (선택) 송출 도구 — OBS 등 RTMP 송출 프로그램. 실제 영상을 내보낼 때 필요합니다.
Base URL
| 구분 | Base URL | 비고 |
|---|---|---|
| 운영 | https://private.shopliveapi.com | 환경은 하나입니다. 테스트는 운영 계정에서 리허설 방송·테스트용 상품으로 진행하세요. |
모든 v2 엔드포인트는 /v2/{accessKey}/... 형태입니다. 경로의 {accessKey} 와 토큰 안의 accessKey 클레임이 일치해야 합니다.
세 가지 주제
다음 → 인증 (JWT)
인증 (JWT)
External API 는 고객사가 직접 서명한 JWT 로 인증합니다. 별도의 토큰 발급 API 를 호출할 필요가 없습니다. secretKey 로 HS256 서명한 토큰을 만들어 Authorization: Bearer 헤더에 담으면 됩니다.
토큰 규격
| 항목 | 값 | 설명 |
|---|---|---|
| 알고리즘 | HS256 | 서명 키는 secretKey 입니다. |
클레임 accessKey | 필수 | 고객사 accessKey. 같은 계정의 secretKey 로 서명하세요. |
클레임 exp | 필수 | 만료 시각. 서버 간 통신용이므로 충분히 길게(예: 1~3년) 잡고 서버에서 재사용해도 됩니다. |
클레임 iat | 권장 | 발급 시각. |
| 헤더 | Authorization: Bearer {JWT} | 모든 요청에 포함합니다. |
secretKey 는 Base64 로 인코딩된 키입니다. Java 의 JJWT(0.9.x) 는 문자열 키를 자동으로 Base64 디코딩하지만, Node.js · Python 등 다른 라이브러리는 디코딩한 바이트를 서명 키로 넘겨야 서버 검증과 일치합니다. 아래 예제는 이 차이를 반영했습니다.토큰 생성 예제
// npm i jsonwebtoken
const jwt = require('jsonwebtoken');
function createShopliveToken(accessKey, base64SecretKey) {
const key = Buffer.from(base64SecretKey, 'base64'); // 디코딩한 바이트로 서명
return jwt.sign({ accessKey }, key, { algorithm: 'HS256', expiresIn: '365d' });
}
# pip install pyjwt
import base64, time, jwt
def create_shoplive_token(access_key: str, base64_secret_key: str) -> str:
key = base64.b64decode(base64_secret_key) # 디코딩한 바이트로 서명
now = int(time.time())
payload = {"accessKey": access_key, "iat": now, "exp": now + 365 * 24 * 3600}
return jwt.encode(payload, key, algorithm="HS256")
// build.gradle: implementation 'io.jsonwebtoken:jjwt:0.9.1'
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
public class ShopliveToken {
public static String create(String accessKey, String base64SecretKey) {
long oneYear = 365L * 24 * 60 * 60 * 1000;
return Jwts.builder()
.signWith(SignatureAlgorithm.HS256, base64SecretKey) // 문자열 키는 Base64 로 해석됨
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + oneYear))
.claim("accessKey", accessKey)
.compact();
}
}
// go get github.com/golang-jwt/jwt/v5
package shoplive
import (
"encoding/base64"
"time"
"github.com/golang-jwt/jwt/v5"
)
func CreateShopliveToken(accessKey, base64SecretKey string) (string, error) {
key, err := base64.StdEncoding.DecodeString(base64SecretKey) // 디코딩한 바이트로 서명
if err != nil {
return "", err
}
now := time.Now()
claims := jwt.MapClaims{
"accessKey": accessKey,
"iat": now.Unix(),
"exp": now.Add(365 * 24 * time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(key)
}
# gem install jwt
require 'jwt'
require 'base64'
def create_shoplive_token(access_key, base64_secret_key)
key = Base64.decode64(base64_secret_key) # 디코딩한 바이트로 서명
now = Time.now.to_i
payload = { accessKey: access_key, iat: now, exp: now + 365 * 24 * 3600 }
JWT.encode(payload, key, 'HS256')
end
첫 호출로 확인하기
생성한 토큰으로 방송 목록을 조회해 인증이 되는지 확인합니다. 예제의 YOUR_JWT 자리에 토큰을 넣으세요. 토큰은 시크릿이므로 오른쪽 패널에서는 받지 않습니다.
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign?page=1&count=5" \
-H "Authorization: Bearer YOUR_JWT"정상이면 200 과 함께 results 배열이 옵니다(방송이 없으면 빈 배열). 실패하면 401 과 아래와 같은 본문이 옵니다.
{ "_s": -824, "_e": "Invalid token" }Token does not exist.— Authorization 헤더가 없거나 값이 비어 있습니다. 헤더를Authorization: Bearer <토큰>형식으로 보냈는지 확인하세요.Invalid token— 서명 불일치(secretKey 오류·Base64 처리 누락),accessKey클레임 누락, 존재하지 않는 accessKey.Expired token(_s: -412) —exp가 지났습니다. 토큰을 다시 만드세요.- 토큰이 맞는데도
_s: -824가 계속되면 — URL 의 accessKey 가 토큰의accessKey클레임과 같은지 확인하세요. 같다면 계정에 External API v2 사용이 활성화돼 있지 않을 수 있으니 담당 매니저에게 활성화를 요청하세요.
요청·응답 규약
모든 엔드포인트에 공통으로 적용되는 규칙입니다. 한 번 익혀두면 레퍼런스의 어떤 API 도 같은 방식으로 호출할 수 있습니다.
형식
- 요청·응답 본문은 JSON (UTF-8) 입니다. 본문이 있는 요청에는
Content-Type: application/json을 붙입니다. - 시각은 ISO 8601 날짜·시각 문자열(UTC 오프셋 포함)입니다. 예:
2026-10-01T20:00:00+09:00,2026-10-01T11:00:00Z. 응답은 UTC(Z) 기준으로 내려옵니다. - 성공 응답은 HTTP
200또는204이며, 반환할 내용이 없는 API(방송 시작·종료 등)는 빈 본문을 돌려줍니다.
식별자
| 식별자 | 설명 |
|---|---|
accessKey | 고객사 키. 모든 경로의 첫 세그먼트. |
campaignKey / campaignId | 방송 식별자. 경로의 {campaignKeyOrId} 자리에는 둘 중 어느 것을 넣어도 됩니다. 플레이어·SDK 연동에는 campaignKey 를 사용합니다. |
productId / customerProductId | 상품 식별자. customerProductId 는 고객사 시스템의 상품 ID 로, 등록 시 넣어두면 이후 중복 체크·수정·연결의 기준이 됩니다. |
sellerId, brandId | 셀러·브랜드 식별자. 셀러 단위로 방송·상품을 나눌 때 사용합니다. |
목록 조회와 페이지네이션
목록 API 는 공통 쿼리 파라미터를 받고 results + totalCount 형태로 응답합니다.
| 파라미터 | 기본값 | 설명 |
|---|---|---|
page | 1 | 1부터 시작하는 페이지 번호. |
count | 10 | 페이지 크기. 방송 목록은 최대 20입니다. 기본값과 상한은 API마다 다릅니다(상품 목록은 기본 30 · 최대 100). |
keyword | — | 제목 등 텍스트 검색. 상품 목록은 keyword 대신 name·brand·sku·url 로 검색합니다. |
sortAscending | false | 정렬 방향. 방송 목록은 예약 시각 기준입니다. |
{
"results": [ { "...": "항목" } ],
"totalCount": 42
}여러 건 처리 응답
상품 등록·수정·연결처럼 배열을 받는 API 는 항목별 결과를 돌려줍니다. HTTP 는 200 이어도 일부가 실패할 수 있으니 failCount 를 확인하세요.
{
"results": [
{ "customerProductId": "SKU-1001", "status": "CREATE" },
{ "customerProductId": "SKU-1002", "status": "FAIL", "message": "..." }
],
"successCount": 1, "warnCount": 0, "failCount": 1, "totalCount": 2
}에러 응답
HTTP 상태 코드가 400 이상이거나, 응답 본문(JSON)에 음수 _s 가 있으면 에러입니다. 상태 코드와 _s 를 둘 다 확인하세요. 성공 응답에는 음수 _s 가 없습니다.
{
"_s": -710,
"_e": "Campaign does not exist"
}_s 는 Shoplive 상태 코드(음수), _e 는 사람이 읽는 메시지입니다. 응답에 따라 본문이 비어 있거나 다른 형태일 수 있습니다.
자주 만나는 코드는 에러 코드 절에 정리했습니다. 클라이언트는 HTTP 상태 코드와 _s 를 둘 다 확인해 에러를 판정하고, _s·_e(또는 message)는 로그와 문의 시 참고 값으로 남겨두세요. 상품 일괄 API 처럼 HTTP 200 이면서 항목별 실패를 status: "FAIL" 로 알리는 응답도 있으니 failCount 와 항목별 message 도 확인하세요.
첫 방송 만들기
API 로 방송을 만들고 상품을 붙인 뒤, 내 웹 페이지에 플레이어를 띄워 방송이 보이는 것까지 확인합니다. 방송 시작·종료는 진행자가 Shoplive 콘솔에서 하는 것이 일반적이라 마지막에 선택 단계로 두었습니다.
- 미션 1 · 방송 생성 — 제목과 예약 시각으로 방송을 만들고
campaignKey를 받습니다. - 미션 2 · 상품 등록 — 고객사 상품 ID(
customerProductId)로 상품을 등록합니다. - 미션 3 · 방송에 상품 연결 — 등록한 상품을 방송에 붙이고 순서를 정합니다.
- 미션 4 · 송출 정보 확인 — RTMP 주소·스트림 키·플레이어 URL 을 받습니다.
- 미션 5 · 내 페이지에 플레이어 노출 — 웹 SDK 코드 몇 줄로 방송을 띄웁니다.
- 미션 6 · (선택) API 로 시작·종료 — 콘솔 대신 자동화가 필요할 때.
이전 ← 규약다음 → 미션 1 · 방송 생성
미션 1 · 방송 생성
방송(campaign)은 Shoplive 의 기본 단위입니다. 제목 하나만 있으면 만들 수 있고, 생성 즉시 플레이어 URL 과 송출 정보가 함께 발급됩니다.
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{
"campaignMeta": {
"title": "가을 신상품 라이브",
"description": "10월 1일 저녁 8시",
"tags": ["autumn", "new"],
"serveReplay": true,
"scheduledStartAt": "2026-10-01T20:00:00+09:00",
"scheduledEndAt": "2026-10-01T21:00:00+09:00"
}
}'| 필드 | 필수 | 설명 |
|---|---|---|
campaignMeta.title | 필수 | 방송 제목. 없으면 _s: -837 로 실패합니다. |
campaignMeta.description | 내부 메모. | |
campaignMeta.tags | 검색·분류용 태그 배열. | |
campaignMeta.serveReplay | 종료 후 다시보기 공개 여부. | |
campaignMeta.scheduledStartAt / scheduledEndAt | 예약 시각. 플레이어의 방송 예정 표시에 사용됩니다. | |
campaignMeta.rehearsalPasscode | 리허설 입장 코드. | |
seller.sellerId, campaignMeta.brand.brandId | 셀러·브랜드 소속으로 만들 때 지정합니다. |
{
"campaignMeta": {
"campaignId": "123456",
"campaignKey": "a1b2c3d4e5f6",
"shoplivePlayerUrl": "https://player.shoplive.cloud/...",
"title": "가을 신상품 라이브",
"campaignStatus": "READY",
"serveReplay": true,
"tags": ["autumn", "new"],
"scheduledStartAt": "2026-10-01T11:00:00Z",
"scheduledEndAt": "2026-10-01T12:00:00Z"
},
"stream": {
"streamUrl": "rtmp://...",
"streamKey": "live_...",
"liveUrl": "https://.../playlist.m3u8",
"videoAspectRatio": "9:16"
}
}응답의 campaignMeta.campaignKey 를 저장해 두세요. 이후 모든 단계에서 사용합니다. 오른쪽 패널의 campaignKey 칸에 넣으면 아래 예제에 바로 반영됩니다.
방송 상태(campaignStatus)
| 상태 | 의미 |
|---|---|
RESERVED | 자동 시작이 예약된 상태. |
READY | 방송 시작 가능. 생성 직후 기본 상태. 플레이어는 예정 화면을 보여줍니다. |
REHEARSAL | 리허설 진행 중. 입장 코드(rehearsalPasscode)를 아는 사람만 볼 수 있음. end 로 끝내면 READY 로 돌아감. |
ONAIR | 라이브 진행 중. |
ENDED | 종료됨. serveReplay 가 참이면 다시보기 제공. |
이전 ← 첫 방송 만들기다음 → 미션 2 · 상품 등록
미션 2 · 상품 등록
상품은 고객사 단위로 한 번 등록해 두고 여러 방송에서 재사용합니다. 배열로 한 번에 여러 개를 등록할 수 있습니다.
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/product" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[
{
"customerProductId": "SKU-1001",
"name": "울 블렌드 코트",
"brand": "Shoplive Apparel",
"url": "https://shop.example.com/products/1001",
"imageUrl": "https://cdn.example.com/1001.jpg",
"currency": "KRW",
"originalPrice": 259000,
"discountPrice": 199000,
"showPrice": true
},
{
"customerProductId": "SKU-1002",
"name": "캐시미어 머플러",
"url": "https://shop.example.com/products/1002",
"imageUrl": "https://cdn.example.com/1002.jpg",
"currency": "KRW",
"originalPrice": 89000
}
]'| 필드 | 필수 | 설명 |
|---|---|---|
customerProductId | 권장 | 고객사 상품 ID(고유값). 이 가이드의 동기화 흐름에서는 항상 지정하세요. 이후 수정·연결·조회의 기준 키이며, 같은 값이 이미 있으면 그 항목은 status: FAIL 로 돌아옵니다. 갱신은 미션 7의 PUT 을 사용합니다. |
name | 필수 | 상품명. |
url | 필수 | 상품 상세 페이지. 플레이어에서 상품을 누르면 이동하는 주소입니다. |
imageUrl | 권장 | 대표 이미지. |
currency, originalPrice, discountPrice, discountRate | 가격 정보. | |
showPrice | 가격 표시 여부(기본 true). 수정(PUT)할 때도 원하는 값을 함께 보내세요. | |
brand, brandIdentifier | 브랜드 표시명 · 고객사 브랜드 코드. | |
sku, description, ctaButtonTitle | 부가 정보. 버튼 문구를 바꾸려면 ctaButtonTitle(사용 전 Shoplive 와 협의 필요). | |
seller.sellerId | 셀러 소유 상품으로 만들 때 지정. 생략하면 고객사 소유. |
{
"results": [
{ "status": "CREATE", "productId": "90001", "customerProductId": "SKU-1001", "name": "울 블렌드 코트", "...": "..." },
{ "status": "CREATE", "productId": "90002", "customerProductId": "SKU-1002", "name": "캐시미어 머플러", "...": "..." }
],
"successCount": 2, "warnCount": 0, "failCount": 0, "totalCount": 2
}이전 ← 미션 1 · 방송 생성다음 → 미션 3 · 방송에 상품 연결
미션 3 · 방송에 상품 연결
등록한 상품을 방송에 붙입니다. 이 API 는 추가 방식입니다. 보낸 상품이 방송 상품 목록의 끝에 순서대로 추가됩니다. 이미 연결된 상품은 다시 추가되지 않습니다.
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY/product" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-1001" }, { "customerProductId": "SKU-1002" } ]'| 필드 | 설명 |
|---|---|
customerProductId 또는 productId | 연결할 상품. 한 요청 안에서는 한 종류만 사용하세요. |
{
"results": [
{ "customerProductId": "SKU-1001", "status": "SUCCESS", "message": null },
{ "customerProductId": "SKU-1002", "status": "FAIL", "message": "The product is already registered." }
],
"successCount": 1, "failCount": 1, "totalCount": 2
}이미 연결된 상품은 항목 결과가 FAIL(The product is already registered.)로 오고, 나머지 항목은 그대로 연결됩니다. 같은 요청을 다시 보내도 됩니다. 순서는 별도 API 로 정합니다. 본문에 나열한 순서대로 정렬되고, 나열하지 않은 상품은 뒤로 밀립니다.
curl -s -X PUT "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY/product/order" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-1002" }, { "customerProductId": "SKU-1001" } ]'이전 ← 미션 2 · 상품 등록다음 → 미션 4 · 송출 정보 확인
미션 4 · 송출 정보 확인
방송 하나를 조회하면 메타 정보와 함께 송출용 RTMP 주소, 시청용 플레이어 URL, 연결된 상품 목록이 모두 내려옵니다.
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY" \
-H "Authorization: Bearer YOUR_JWT"| 경로 | 용도 |
|---|---|
stream.streamUrl + stream.streamKey | OBS 등 송출 프로그램의 서버 주소 · 스트림 키에 입력합니다. 송출을 켜면 콘솔 미리보기에 영상이 보입니다. |
campaignMeta.shoplivePlayerUrl | 바로 열어볼 수 있는 시청 페이지. |
campaignMeta.campaignKey | 다음 단계에서 웹 SDK 에 넘기는 값. |
stream.liveUrl | HLS(m3u8) 주소. 자체 플레이어를 쓸 때 사용합니다. |
campaignProductList[] | 미션 3 에서 연결한 상품과 sortOrder. |
ONAIR 로 바꾸는 것은 다른 일입니다. 보통 송출을 먼저 켜고 콘솔 미리보기로 확인한 뒤, 진행자가 콘솔에서 방송 시작을 누릅니다(API 로는 미션 6).이전 ← 미션 3 · 방송에 상품 연결다음 → 미션 5 · 내 페이지에 플레이어 노출
미션 5 · 내 페이지에 플레이어 노출
API 로 만든 방송은 웹 SDK 코드 몇 줄로 내 페이지에 띄울 수 있습니다. 이 단계까지 오면 첫 방송 만들기가 완료됩니다. 방송이 READY 상태면 예정 화면이, ONAIR 면 라이브가 보입니다.
<script src="https://static.shoplive.cloud/shoplive.js"></script>
<div id="shoplive-player"></div>
<script>
cloud.shoplive.init({ accessKey: "YOUR_ACCESS_KEY" });
cloud.shoplive.setPlayer("shoplive-player", { campaignKey: "CAMPAIGN_KEY" });
</script>- 웹 — 사용자 정보 연결, 장바구니·구매 콜백 등 나머지 연동은 웹 SDK 가이드를 따라가세요.
- Android · iOS — 같은
accessKey와campaignKey로 모바일 SDK에서 플레이어를 띄웁니다. - 방송 목록 페이지 —
GET /v2/{accessKey}/campaign?campaignStatus=READY로 예정 방송을 가져와(어드민에서 저장 영상 송출 방송에 자동 시작을 예약했다면RESERVED로 따로 조회) 자사 UI 에 그리고, 클릭 시 위 코드로 플레이어를 여는 구성이 일반적입니다.
이전 ← 미션 4 · 송출 정보 확인다음 → 미션 6 · (선택) API 로 시작·종료
미션 6 · (선택) API 로 시작·종료
진행자가 콘솔을 쓰지 않고 스케줄러가 방송을 켜고 끄는 구성이라면 콘솔(Console) API 를 사용합니다. 송출 프로그램이 영상을 보내고 있는 상태에서 시작을 호출하면 시청자에게 라이브가 노출됩니다.
# 리허설 시작 (입장 코드를 아는 사람만 시청)
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/start?rehearsal=true" \
-H "Authorization: Bearer YOUR_JWT" -o /dev/null -w "%{http_code}\n"
# 리허설 종료 → READY
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/end" \
-H "Authorization: Bearer YOUR_JWT" -o /dev/null -w "%{http_code}\n"
# 라이브 시작 → ONAIR
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/start" \
-H "Authorization: Bearer YOUR_JWT" -o /dev/null -w "%{http_code}\n"
# 종료 → ENDED
curl -s -X POST "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/end" \
-H "Authorization: Bearer YOUR_JWT" -o /dev/null -w "%{http_code}\n"리허설은 end 로 끝내면 READY 로 돌아갑니다. 그다음 start 를 호출해 라이브를 시작하세요.
ONAIR 가 되지만 시청자에게는 대기 화면이 보입니다. 자동화한다면 송출 프로그램이 스트림을 보내고 있는지(콘솔 미리보기 또는 stream.liveUrl 응답 확인) 먼저 점검하세요.이전 ← 미션 5 · 내 페이지에 플레이어 노출다음 → 상품 카탈로그 동기화
상품 카탈로그 동기화
자사 상품 DB 가 원본이고 Shoplive 는 그 사본이어야 합니다. 상품 카탈로그 동기화는 customerProductId 를 축으로 상품을 만들고·고치고·지우는 동기화 루틴을 만들고, 방송 중 품절·재고 변화를 시청자 화면에 즉시 반영하는 방법을 다룹니다.
미션 7 · 상품 upsert 패턴
상품 등록(POST)은 같은 customerProductId 가 이미 있으면 그 항목을 status: FAIL 로 돌려줍니다. 수정(PUT)은 customerProductId 로 대상을 찾습니다. 두 가지를 조합하면 동기화 루틴이 됩니다.
- 변경된 상품을 모두 POST 로 보냅니다. 신규는
CREATE, 기존은FAIL로 나뉩니다. FAIL로 돌아온 항목을 모아 PUT 으로 다시 보냅니다. 내용이 같아도UPDATE로 응답합니다.message문구는 언어 설정에 따라 달라지니 문자열로 판별하지 마세요.- 자사에서 삭제된 상품은 DELETE 로 정리합니다. 방송에 연결된 상품까지 지우지 않으려면
?force=false를 붙이세요. 이때 방송에 연결된 상품은 삭제되지 않고FAIL로 돌아옵니다. 기본값(true)은 상품을 방송의 상품 목록에서 빼고 삭제합니다.
# 가격 인하 반영: customerProductId 로 대상을 찾아 수정
curl -s -X PUT "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/product" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[
{ "customerProductId": "SKU-1001", "name": "울 블렌드 코트", "url": "https://shop.example.com/products/1001",
"currency": "KRW", "originalPrice": 259000, "discountPrice": 179000, "showPrice": true }
]'# force=false: 방송에 연결된 상품은 지우지 않고 FAIL 로 응답 (기본값 true 는 방송 상품 목록에서 빼고 삭제)
curl -s -X DELETE "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/product?force=false" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-OLD-0001" } ]'아래 예제는 인증 절에서 만든 JWT 를 환경 변수 SL_TOKEN 에 넣어 두었다고 가정합니다.
async function syncProducts(products) { // products: 자사 DB 의 변경분
const created = await sl('POST', '/product', products);
const existing = created.results
.filter(r => r.status === 'FAIL')
.map(r => products.find(p => p.customerProductId === r.customerProductId));
const updated = existing.length ? await sl('PUT', '/product', existing) : { results: [] };
return { created: created.successCount, updated: updated.successCount,
failed: updated.results.filter(r => r.status === 'FAIL') };
}
async function sl(method, path, body) {
const res = await fetch(`https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY${path}`, {
method, headers: { Authorization: `Bearer ${process.env.SL_TOKEN}`, 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
let data = null;
try { data = JSON.parse(text); } catch {} // 본문이 비었거나 JSON 이 아닐 수 있음
if (!res.ok || data?._s < 0) throw new Error(`${res.status} ${text}`); // HTTP ≥400 또는 _s < 0 이면 에러 (에러 응답 절 참고)
return data;
}
import os, requests
BASE = "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY"
H = {"Authorization": f"Bearer {os.environ['SL_TOKEN']}", "Content-Type": "application/json"}
def sl(method, path, body):
r = requests.request(method, BASE + path, json=body, headers=H, timeout=10)
try:
data = r.json()
except ValueError: # 본문이 비었거나 JSON 이 아닐 수 있음
data = None
s = data.get("_s") if isinstance(data, dict) else None
if r.status_code >= 400 or (isinstance(s, (int, float)) and s < 0):
raise RuntimeError(f"{r.status_code} {r.text}") # HTTP ≥400 또는 _s < 0 이면 에러 (에러 응답 절 참고)
return data
def sync_products(products): # products: 자사 DB 의 변경분 (dict 리스트)
created = sl("POST", "/product", products)
by_id = {p["customerProductId"]: p for p in products}
existing = [by_id[r["customerProductId"]] for r in created["results"]
if r["status"] == "FAIL"]
updated = sl("PUT", "/product", existing) if existing else {"results": [], "successCount": 0}
return created["successCount"], updated["successCount"]
// build.gradle: implementation 'org.json:json:20240303'
import org.json.JSONArray;
import org.json.JSONObject;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class ProductSync {
static final HttpClient CLIENT = HttpClient.newHttpClient();
static JSONObject syncProducts(JSONArray products) throws Exception { // products: 자사 DB 의 변경분
JSONObject created = sl("POST", "/product", products);
JSONArray createdResults = created.getJSONArray("results");
JSONArray existing = new JSONArray();
for (int i = 0; i < createdResults.length(); i++) {
JSONObject r = createdResults.getJSONObject(i);
if ("FAIL".equals(r.getString("status"))) {
String cid = r.getString("customerProductId");
for (int j = 0; j < products.length(); j++) {
JSONObject p = products.getJSONObject(j);
if (cid.equals(p.getString("customerProductId"))) { existing.put(p); break; }
}
}
}
JSONObject updated = existing.length() > 0
? sl("PUT", "/product", existing)
: new JSONObject().put("results", new JSONArray()).put("successCount", 0);
JSONArray failed = new JSONArray();
JSONArray updatedResults = updated.getJSONArray("results");
for (int i = 0; i < updatedResults.length(); i++) {
JSONObject r = updatedResults.getJSONObject(i);
if ("FAIL".equals(r.getString("status"))) failed.put(r);
}
return new JSONObject()
.put("created", created.getInt("successCount"))
.put("updated", updated.getInt("successCount"))
.put("failed", failed);
}
static JSONObject sl(String method, String path, Object body) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY" + path))
.header("Authorization", "Bearer " + System.getenv("SL_TOKEN"))
.header("Content-Type", "application/json")
.method(method, HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
String text = response.body() == null ? "" : response.body().trim();
JSONObject json = text.startsWith("{") ? new JSONObject(text) : new JSONObject(); // 본문이 비었거나 JSON 이 아닐 수 있음
if (response.statusCode() >= 400 || json.optInt("_s", 0) < 0) {
throw new RuntimeException(response.statusCode() + " " + text); // HTTP ≥400 또는 _s < 0 이면 에러 (에러 응답 절 참고)
}
return json;
}
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type slResult struct {
CustomerProductID string `json:"customerProductId"`
Status string `json:"status"`
}
type slResponse struct {
S int `json:"_s"`
Results []slResult `json:"results"`
SuccessCount int `json:"successCount"`
}
// products: 자사 DB 의 변경분
func syncProducts(products []map[string]interface{}) (map[string]interface{}, error) {
created, err := sl("POST", "/product", products)
if err != nil {
return nil, err
}
var existing []map[string]interface{}
for _, r := range created.Results {
if r.Status == "FAIL" {
for _, p := range products {
if p["customerProductId"] == r.CustomerProductID {
existing = append(existing, p)
break
}
}
}
}
var updated slResponse
if len(existing) > 0 {
u, err := sl("PUT", "/product", existing)
if err != nil {
return nil, err
}
updated = *u
}
var failed []slResult
for _, r := range updated.Results {
if r.Status == "FAIL" {
failed = append(failed, r)
}
}
return map[string]interface{}{
"created": created.SuccessCount,
"updated": updated.SuccessCount,
"failed": failed,
}, nil
}
func sl(method, path string, body interface{}) (*slResponse, error) {
buf, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(method, "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY"+path, bytes.NewReader(buf))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("SL_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
var out slResponse
jsonErr := json.Unmarshal(data, &out) // 본문이 비었거나 JSON 이 아닐 수 있음
if res.StatusCode >= 400 || (jsonErr == nil && out.S < 0) {
return nil, fmt.Errorf("%d %s", res.StatusCode, string(data)) // HTTP ≥400 또는 _s < 0 이면 에러 (에러 응답 절 참고)
}
if jsonErr != nil {
return nil, jsonErr
}
return &out, nil
}
require 'net/http'
require 'json'
require 'uri'
# products: 자사 DB 의 변경분
def sync_products(products)
created = sl('POST', '/product', products)
existing = created['results']
.select { |r| r['status'] == 'FAIL' }
.map { |r| products.find { |p| p['customerProductId'] == r['customerProductId'] } }
updated = existing.any? ? sl('PUT', '/product', existing) : { 'results' => [], 'successCount' => 0 }
{
'created' => created['successCount'],
'updated' => updated['successCount'],
'failed' => updated['results'].select { |r| r['status'] == 'FAIL' }
}
end
def sl(method, path, body)
uri = URI("https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY#{path}")
req = Net::HTTP.const_get(method.capitalize).new(uri)
req['Authorization'] = "Bearer #{ENV['SL_TOKEN']}"
req['Content-Type'] = 'application/json'
req.body = body.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body.to_s) rescue nil # 본문이 비었거나 JSON 이 아닐 수 있음
s = data.is_a?(Hash) ? data['_s'] : nil
raise "#{res.code} #{res.body}" if res.code.to_i >= 400 || (s.is_a?(Numeric) && s < 0) # HTTP ≥400 또는 _s < 0 이면 에러 (에러 응답 절 참고)
data
end
status 를 동기화 로그에 남기세요. 값은 API 마다 다릅니다: 등록 CREATE·WARN·FAIL / 수정 UPDATE·FAIL / 삭제 DELETE·FAIL. WARN 은 상품은 만들어졌지만 확인이 필요한 경우입니다.이전 ← 상품 카탈로그 동기화다음 → 미션 8 · 방송 상품 갱신
미션 8 · 방송 상품 갱신
방송에 붙은 상품 목록은 네 가지 동작으로 관리합니다. 전체 해제를 뺀 세 동작은 customerProductId 배열을 본문으로 받습니다.
| 동작 | 엔드포인트 | 비고 |
|---|---|---|
| 추가 | POST /v2/{accessKey}/campaign/{campaignKeyOrId}/product | 끝에 추가. 이미 있으면 항목 FAIL. |
| 제외 | DELETE /v2/{accessKey}/campaign/{campaignKeyOrId}/product | 방송에서만 빼고 상품 자체는 유지. |
| 순서 변경 | PUT /v2/{accessKey}/campaign/{campaignKeyOrId}/product/order | 나열 순서대로 정렬, 미나열은 뒤로. |
| 전체 해제 | DELETE /v2/{accessKey}/campaign/{campaignKeyOrId}/product/all | 목록을 비우고 다시 구성할 때. |
# 방송에서 상품 하나 제외
curl -s -X DELETE "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY/product" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-1002" } ]'"방송의 상품 목록을 자사 기획 화면과 똑같이 맞추기"가 목표라면 전체 해제 → 추가 → 순서 변경 세 번 호출이 가장 단순합니다. 방송 중에는 목록이 잠깐 비는 순간이 생기므로 방송 전에 하거나, 차이만 계산해 추가·제외하세요.
이전 ← 미션 7 · 상품 upsert 패턴다음 → 미션 9 · 품절·재고 실시간 반영
미션 9 · 품절·재고 실시간 반영
방송 중 재고가 바뀌면 콘솔 API 로 해당 방송의 상품 상태를 바꿉니다. 시청자 플레이어에 즉시 반영됩니다. 상품 자체(가격·이름)를 고치는 PUT 과 달리, 이 API 는 특정 방송 안에서의 표시 상태만 다룹니다.
# 품절 처리
curl -s -X PUT "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/product/stockStatus?stockStatus=SOLD_OUT" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-1002" } ]'
# 재입고 → 판매중으로 되돌리기
curl -s -X PUT "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/product/stockStatus?stockStatus=IN_STOCK" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-1002" } ]'
# 지금 소개 중(Now) 강조 켜기
curl -s -X PUT "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/console/CAMPAIGN_KEY/product/show?show=true" \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '[ { "customerProductId": "SKU-1001" } ]'stockStatus | 표시 |
|---|---|
IN_STOCK | 판매중. |
LOW_IN_STOCK | 품절 임박 표시. |
SOLD_OUT | 품절. 구매 버튼이 비활성화됩니다. |
ONAIR 인 방송만 대상으로 하면 호출량을 줄일 수 있습니다.이전 ← 미션 8 · 방송 상품 갱신다음 → 미션 10 · 셀러별로 나누기
미션 10 · 셀러별로 나누기
입점사(셀러)별로 방송과 상품을 분리 운영하는 마켓플레이스형 고객사라면, 리소스를 만들 때 seller.sellerId 를 지정하고 목록 조회에 sellerId 필터를 씁니다. 이미 만든 리소스는 소유권 이전 API 로 옮길 수 있습니다.
| 하고 싶은 일 | 방법 |
|---|---|
| 셀러 만들기·조회 | Seller API — POST /v2/{accessKey}/seller |
| 셀러 소유로 상품·방송 생성 | 본문에 "seller": { "sellerId": 123 } |
| 기존 상품·방송을 셀러로 이전 | PUT /v2/{accessKey}/product/transferToSeller/{sellerId}, PUT /v2/{accessKey}/campaign/{campaignKeyOrId}/transferToSeller/{sellerId}. 방송에 연결된 상품까지 함께 옮기려면 방송 이전에 ?withProduct=true 를 붙이세요. |
| 셀러의 것만 조회 | 방송·상품 목록 API 에 sellerId=123 |
이전 ← 미션 9 · 품절·재고 실시간 반영다음 → 방송 결과 수집
방송 결과 수집
방송이 끝나면 시청·상호작용 지표와 원본 로그를 자사 데이터 웨어하우스로 가져오고, 다시보기 URL 을 자사 페이지에 연결합니다. 핵심은 폴링 대신 웹훅으로 종료를 감지하는 것입니다.
이전 ← 미션 10 · 셀러별로 나누기다음 → 미션 11 · 웹훅으로 종료 감지
미션 11 · 웹훅으로 종료 감지
웹훅은 Shoplive 가 고객사 엔드포인트로 방송 이벤트를 보내주는 기능입니다. 설정은 셀프서비스가 아니라 Shoplive 측 등록이 필요하므로 담당 매니저 또는 ask@shoplive.cloud 로 수신 URL 을 전달하세요.
| 이벤트 | 활용 |
|---|---|
| 방송 시작 | 자사 페이지 "LIVE" 배지 켜기, 푸시 발송 |
| 방송 종료 | 이 주제의 트리거. 통계·로그 수집, 다시보기 게시 |
| 경매 시작 · 종료 · 리셋 | 경매형 방송의 낙찰 처리 |
- 수신 서버는 약 1초 안에 200 을 돌려주고 실제 처리는 큐에 넣어 비동기로 하세요. 종료 직후에는 집계가 완료되기까지 수 분이 걸릴 수 있어 수집 작업을 조금 뒤로 예약하는 것이 안전합니다.
- 웹훅은 이벤트마다 한 번만 전송됩니다. 아래 폴링을 주기적인 보정(reconciliation) 작업으로 함께 돌리고, 두 경로로 같은 방송이 들어올 수 있으니
campaignKey기준으로 멱등하게 처리하세요. - 시작·종료 이벤트의 본문은 방송 API 가 돌려주는 Campaign 객체와 같습니다.
campaignMeta.campaignKey,campaignMeta.campaignStatus… 로 읽습니다. 시작·종료 이벤트는 각각 다른 URL 로, 경매 이벤트는 세 번째 URL 로(받을 이벤트 종류도 설정 가능) 전송됩니다. - 서명 검증(시작·종료 이벤트만, 웹훅 시크릿 키를 설정한 경우): 요청 헤더
X-SHOPLIVE-WEBHOOK-TIMESTAMP(Unix epoch 밀리초 문자열)와X-SHOPLIVE-WEBHOOK-SIGNATURE(HMAC-SHA256 의 소문자 hex)가 함께 옵니다. 서명 대상은 타임스탬프 +.+ 받은 그대로의 요청 본문 바이트(JSON 파싱 전)이고, 키는 시크릿 문자열의 UTF-8 바이트입니다. 비교는 상수 시간 비교로 하세요. - 리허설 방송은 등록 시 해당 옵션을 켠 경우에만 웹훅을 보냅니다.
- 필드 상세는 Webhook 레퍼런스를 참고하세요.
서명 검증 예제입니다. WEBHOOK_SECRET 은 등록 시 설정한 웹훅 시크릿 키이고, 본문은 파싱하기 전의 원본 바이트로 검증합니다.
const crypto = require('crypto');
const express = require('express');
const app = express();
// Express: 서명 검증은 파싱 전 원본 Buffer 로 해야 합니다
app.post('/webhooks/shoplive', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-SHOPLIVE-WEBHOOK-TIMESTAMP') || '';
const sig = req.get('X-SHOPLIVE-WEBHOOK-SIGNATURE') || '';
const expected = crypto.createHmac('sha256', process.env.WEBHOOK_SECRET) // 시크릿 문자열 (UTF-8)
.update(Buffer.concat([Buffer.from(ts + '.'), req.body])).digest('hex');
const ok = sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
res.sendStatus(200); // 약 1초 안에 응답하고, 처리는 큐로 넘기세요
const body = JSON.parse(req.body); // body.campaignMeta.campaignKey
});
import hashlib, hmac, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/webhooks/shoplive")
def shoplive_webhook():
raw = request.get_data() # 파싱 전 원본 바이트
ts = request.headers.get("X-SHOPLIVE-WEBHOOK-TIMESTAMP", "")
sig = request.headers.get("X-SHOPLIVE-WEBHOOK-SIGNATURE", "")
expected = hmac.new(os.environ["WEBHOOK_SECRET"].encode(),
ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
return "", 200 # 약 1초 안에 200 을 돌려주고, 처리는 큐로 넘기세요
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat; // Java 17+
static boolean verify(String secret, String timestamp, byte[] rawBody, String signature) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
mac.update((timestamp + ".").getBytes(StandardCharsets.UTF_8));
String expected = HexFormat.of().formatHex(mac.doFinal(rawBody));
return MessageDigest.isEqual( // 상수 시간 비교
expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8));
}
종료된 방송을 주기적으로 조회하면 웹훅을 아직 등록하지 못한 동안의 대안이 되고, 등록한 뒤에도 놓친 이벤트를 메우는 보정 작업으로 쓸 수 있습니다. 몇 분 간격이면 충분합니다.
# 최근 하루 동안 예약됐고 지금 ENDED 인 방송 (from 은 예약 시각 기준)
curl -s -G "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign" \
-H "Authorization: Bearer YOUR_JWT" \
--data-urlencode "campaignStatus=ENDED" \
--data-urlencode "from=$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '1 day ago' +%Y-%m-%dT%H:%M:%SZ)" \
--data-urlencode "count=20"이전 ← 방송 결과 수집다음 → 미션 12 · 통계 조회
미션 12 · 통계 조회
방송 하나의 요약 지표는 방송 통계 API 로, 여러 방송을 한 번에 가져올 때는 기간 조회 API 를 사용합니다.
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY/stats" \
-H "Authorization: Bearer YOUR_JWT"통계는 방송이 종료된 뒤에 조회할 수 있습니다. 진행 중이거나 종료 직후 집계 전에는 404 로 응답합니다.
{
"campaignId": "123456",
"campaignKey": "a1b2c3d4e5f6",
"title": "가을 신상품 라이브",
"campaignStartedAt": "2026-10-01T11:00:12Z",
"campaignEndedAt": "2026-10-01T12:03:40Z",
"totalViewers": 8421,
"totalLoginViewers": 5210,
"newLoginViewers": 1830,
"peakConcurrentViewers": 1320,
"averageViewingTime": 412,
"totalViewingTime": 3469452,
"likes": 25630,
"chats": 4180,
"chatViewers": 910,
"productClicks": 2760,
"productClickViewers": 1540,
"bannerClicks": 130
}| 필드 | 설명 |
|---|---|
totalViewers | 고유 시청자 수. |
totalLoginViewers | 로그인(사용자 정보 연결) 시청자 수. SDK 에서 사용자 정보를 넘긴 경우에만 집계됩니다. |
newLoginViewers | 이 고객사의 방송을 처음 시청한 시청자 수. |
peakConcurrentViewers | 최대 동시 시청자. |
averageViewingTime, totalViewingTime | 평균·총 시청 시간(초). |
likes, chats, productClicks, bannerClicks | 상호작용 횟수. *Viewers 는 해당 행동을 한 고유 시청자 수. |
# 지난 7일 안에 종료된 방송들의 통계를 한 번에 (최대 일주일 범위)
curl -s -G "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/stats" \
-H "Authorization: Bearer YOUR_JWT" \
--data-urlencode "from=2026-09-25T00:00:00Z" \
--data-urlencode "to=2026-10-02T00:00:00Z"기간 조회의 from/to 는 방송 종료 시각 기준입니다.
이전 ← 미션 11 · 웹훅으로 종료 감지다음 → 미션 13 · 로그 다운로드
미션 13 · 로그 다운로드
요약 지표만으로 부족하다면 이벤트 단위 원본 로그를 받습니다. 방송 하나의 로그는 ck 로, 여러 방송은 from/to(최대 1주일)로 지정합니다.
# 방송 하나의 상품 클릭 로그를 CSV 로 (ck 지정 시 방송 시작 시각부터 4일 후까지 자동)
curl -s -G "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/log" \
-H "Authorization: Bearer YOUR_JWT" \
--data-urlencode "type=productClickLog" \
--data-urlencode "format=csv" \
--data-urlencode "ck=CAMPAIGN_KEY" \
-o product_clicks.csv| 파라미터 | 값 |
|---|---|
type | likeLog · chatLog · productClickLog · popupClickLog · viewerAccessLog · viewerActivityLog · highlightedChatLog · filteredChatLog |
format | json · csv · tsv |
from, to | ISO 8601. ck 없이 조회할 때 필수이며 범위는 최대 1주일. |
ck | 방송 키 또는 ID 로 한정(선택). 지정하면 from/to 대신 방송 시작 시각부터 4일 후까지의 범위가 적용됩니다. 시작된 방송만 지정할 수 있습니다. |
excludeAdmin | 기본 true. viewerAccessLog·viewerActivityLog 에서 관리자·운영자의 활동을 제외합니다. 채팅은 필터링하지 않습니다. |
이전 ← 미션 12 · 통계 조회다음 → 미션 14 · 다시보기 URL
미션 14 · 다시보기 URL
방송 생성 시 serveReplay: true 였다면 종료 후 다시보기가 자동으로 만들어집니다. 첫 방송 만들기의 웹 SDK 코드는 종료된 방송에서 그대로 다시보기를 재생하므로 별도 작업이 없어도 됩니다. 자체 플레이어나 다른 채널에 쓰려면 주소를 꺼내옵니다.
# 종료된 방송의 다시보기 주소
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY" \
-H "Authorization: Bearer YOUR_JWT" | jq '.stream.replayLiveUrl'stream.replayLiveUrl— 다시보기 스트림 주소. 종료 직후에는 아직 준비 중일 수 있으니 비어 있으면 잠시 후 다시 조회하세요.campaignMeta.shoplivePlayerUrl— Shoplive 플레이어로 다시보기를 여는 링크. 공유·푸시에 바로 쓸 수 있습니다.
이전 ← 미션 13 · 로그 다운로드다음 → 에러 코드
에러 코드
자주 만나는 상태 코드입니다. 본문 _s 값 기준이며, HTTP 상태 코드가 400 이상이거나 _s 가 음수이면 에러로 처리하세요(에러 응답). 상태 코드만 보고 분기하지 말고 _s 도 함께 확인하세요.
| HTTP | _s | 의미 | 대응 |
|---|---|---|---|
| 401 | -824 | 인증 실패 (토큰 없음·서명 불일치·URL 과 토큰의 accessKey 불일치) 또는 계정에 External API v2 사용이 활성화돼 있지 않음 | 인증 절의 체크리스트 확인. 토큰이 맞는데도 계속되면 담당 매니저에게 API v2 활성화 요청 |
| 401 | -412 | 토큰 만료 | 토큰 재생성 |
| 400 | -510 | 잘못된 요청 (필수 필드 누락, 형식 오류) | _e 메시지 참고 |
| 400 | -836 / -837 | 방송 생성 시 campaignMeta / title 누락 | 본문 확인 |
| 200 | — | 상품 검증 실패 (name · url 누락, 가격·할인율 음수). HTTP 에러가 아니라 일괄 응답 안에서 항목별 status: "FAIL" + message 로 옵니다 | failCount 와 results[].message 확인 |
| 400 | -839 | 방송 상품 연결 시 productId / customerProductId 둘 다 없음 | 본문 확인 |
| 403 | -714 · -717 · -711 · -114 · -706 | 지금 상태에서는 할 수 없는 요청 (진행 중인 방송 start -714 · 종료된 방송 start/end -717 · 시작 전 방송 end·로그 조회 -711 · 시작된 방송 삭제 -114 · 재방송 원본 방송 삭제 -706) | 방송 상태(campaignStatus) 확인 후 재시도 |
| 404 | -710 | 방송을 찾을 수 없음 | campaignKeyOrId 확인 |
| 404 | -831 | 상품을 찾을 수 없음 | productId/customerProductId 확인 |
| 404 | -207 | 셀러를 찾을 수 없음 | sellerId 확인 |
| 404 | -404 | 통계가 아직 없음 (진행 중 또는 집계 전) | 몇 분 뒤 다시 조회 |
| 406 | -1009 | 품절 상품의 경매 시작 | 재고 상태 확인 |
| 409 | -1002 · -1004 · -116 | 지금 상태와 맞지 않는 요청 (이미 진행 중이거나 끝난 경매, 노출 중인 팝업 연결 해제 등) | 상태 확인 후 다시 요청 |
| 500 | — | 서버 오류 | 재시도 후 지속되면 요청 시각과 엔드포인트를 첨부해 문의 |
이전 ← 미션 14 · 다시보기 URL다음 → 다음 단계
다음 단계
세 가지 주제를 모두 마쳤다면 운영에 필요한 나머지 기능은 레퍼런스에서 같은 규약으로 호출할 수 있습니다.
| 하고 싶은 일 | API |
|---|---|
| 지난 방송을 재방송·복제 | Campaign API 의 rerun · duplicate |
| 브랜드 관리 | Brand API |
| 숏폼 영상 등록·상품 연결 | Shortform API |
| VOD 영상 관리 | Video API |
| 방송 중 쿠폰 팝업 | Popup API |
| 웹·앱 화면 연동 심화 | 웹 SDK 가이드 · 모바일 SDK 가이드 |
이전 ← 에러 코드다음 → 트러블슈팅 & FAQ
트러블슈팅 & FAQ
토큰을 만들었는데 계속 401 Invalid token 이 납니다.
순서대로 확인하세요. ① 헤더가 Authorization: Bearer <토큰> 형식인지(대소문자·공백). ② 페이로드에 accessKey 클레임이 있는지. ③ 경로의 {accessKey} 와 클레임 값이 같은지. ④ Java 이외 언어에서 secretKey 를 Base64 디코딩한 바이트로 서명했는지. ⑤ 모두 맞는데도 계속되면 계정에 API v2 가 활성화돼 있는지 담당 매니저에게 확인하세요. jwt.io 에 토큰을 붙여 페이로드를 눈으로 확인하는 것이 가장 빠릅니다.
토큰은 얼마나 자주 갱신해야 하나요?
서버 간 통신용이라 짧게 만들 이유가 없습니다. 1~3년으로 만들어 서버 설정에 보관하고, 만료 전에 교체하는 운영 절차만 두면 됩니다. 만료되면 _s: -412 로 알려줍니다. 단, 토큰이 유출되면 secretKey 를 재발급해야 하니 프론트엔드·앱 번들에는 절대 넣지 마세요.
campaignKey 와 campaignId 중 무엇을 저장해야 하나요?
API 경로에는 둘 다 쓸 수 있습니다. 플레이어·SDK 연동과 시청 URL 에는 campaignKey 가 쓰이므로 campaignKey 를 기본 키로 저장하고, campaignId 는 통계·로그 매칭용으로 함께 보관하는 것을 권장합니다.
목록 API 에 count=100 을 넣었는데 20건만 옵니다.
방송 목록은 한 번에 최대 20건까지 조회됩니다(상한은 API마다 다릅니다). totalCount 를 보고 page 를 늘려 순회하세요. 대량 동기화는 from/to 로 기간을 나누면 빠릅니다.
상품 등록(POST)을 다시 보냈는데 FAIL 이 옵니다.
같은 customerProductId 가 이미 있어서입니다. 중복 생성을 막는 정상 동작이며, 내용을 바꾸려면 미션 7의 PUT 을 사용하세요. POST → FAIL 항목만 PUT 으로 재시도하는 패턴이 곧 upsert 입니다.
방송에 상품을 연결했는데 일부가 FAIL 로 옵니다.
message 를 보세요. The product is already registered. 면 이미 연결된 상품이고, The product does not exist. 면 상품 등록이 안 됐거나 customerProductId 오타입니다. 연결 API 는 추가 방식이라 성공한 항목은 그대로 반영되며, 순서는 PUT .../product/order 로 따로 정합니다.
시각을 +09:00 으로 보냈는데 응답은 Z 로 옵니다.
정상입니다. 입력은 오프셋이 있는 ISO 8601 이면 무엇이든 받고, 저장·응답은 UTC 로 통일합니다. 화면에 표시할 때 고객사 시간대로 변환하세요.
start 를 호출해 ONAIR 가 됐는데 플레이어에 영상이 안 나옵니다.
start 는 상태만 바꿉니다. 송출 프로그램이 stream.streamUrl + stream.streamKey 로 실제 영상을 보내고 있는지 확인하세요. 보통 송출을 먼저 켜고 콘솔 미리보기로 확인한 뒤 start 를 호출합니다.
종료 직후 통계를 조회했는데 404 이거나 값이 작거나 0 입니다.
종료 후 집계가 마무리되기까지 수 분이 걸릴 수 있습니다. 웹훅 종료 이벤트를 받으면 바로 조회하지 말고 몇 분 뒤로 예약하거나, 값이 바뀌지 않을 때까지 몇 차례 재조회하세요.
_s·_e 값을 함께 담당 매니저 또는 기술 지원 채널로 보내주세요. 토큰이나 secretKey 원문은 보내지 마세요.