Shoplive Developers
한국어KOEnglishEN日本語JA

Shoplive API 시작 가이드

Shoplive External API v2 를 세 가지 주제로 익힙니다. 인증과 규약을 한 번 익히면, 첫 방송 만들기 → 상품 카탈로그 동기화 → 방송 결과 수집 순서로 실제 운영에 필요한 연동을 완성할 수 있습니다.

따라 하기 팁 화면이 넓다면 오른쪽 패널에 accessKey 와 campaignKey 를 입력하세요. 이 페이지의 모든 예제에 바로 치환되어 복사만 하면 실행됩니다. 아래 폰 화면은 지금 읽는 단계에서 시청자에게 어떻게 보이는지를 보여줍니다. 엔드포인트별 전체 필드는 API 레퍼런스를 참고하세요.
시작하기

개요 & 요구사항

External API v2 는 고객사 시스템(어드민, 백오피스, 커머스 서버)에서 Shoplive 의 방송·상품·셀러·브랜드·숏폼·통계를 서버 대 서버로 다루기 위한 REST API 입니다. Shoplive Admin 화면에서 하는 일의 대부분을 API 로 자동화할 수 있습니다.

대상 독자REST API 와 JWT 에 익숙한 백엔드 개발자
소요 시간첫 방송 만들기는 키가 준비되어 있다면 약 20분, 상품 카탈로그 동기화와 방송 결과 수집은 각각 30분 내외
필요한 것accessKey · secretKey, HTTPS 요청을 보낼 서버 환경

요구사항

  • 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 문자열입니다 Admin 에서 받은 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 자리에 토큰을 넣으세요. 토큰은 시크릿이므로 오른쪽 패널에서는 받지 않습니다.

bash
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign?page=1&count=5" \
  -H "Authorization: Bearer YOUR_JWT"

정상이면 200 과 함께 results 배열이 옵니다(방송이 없으면 빈 배열). 실패하면 401 과 아래와 같은 본문이 옵니다.

401 Unauthorized
{ "_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 형태로 응답합니다.

파라미터기본값설명
page11부터 시작하는 페이지 번호.
count10페이지 크기. 방송 목록은 최대 20입니다. 기본값과 상한은 API마다 다릅니다(상품 목록은 기본 30 · 최대 100).
keyword—제목 등 텍스트 검색. 상품 목록은 keyword 대신 name·brand·sku·url 로 검색합니다.
sortAscendingfalse정렬 방향. 방송 목록은 예약 시각 기준입니다.
목록 응답 형태
{
  "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 에 공개된 고정 한도는 없지만, 같은 리소스를 초 단위로 폴링하는 대신 웹훅으로 상태 변화를 받는 구성을 권장합니다.

이전 ← 인증다음 → 첫 방송 만들기

미션

첫 방송 만들기

API 로 방송을 만들고 상품을 붙인 뒤, 내 웹 페이지에 플레이어를 띄워 방송이 보이는 것까지 확인합니다. 방송 시작·종료는 진행자가 Shoplive 콘솔에서 하는 것이 일반적이라 마지막에 선택 단계로 두었습니다.

완료 조건내 페이지에서 예약된 방송의 플레이어가 열리고, 상품 2개가 목록에 보인다
사용하는 APICampaign · Product · Console(선택) + 웹 SDK
소요 시간약 20분
  1. 미션 1 · 방송 생성 — 제목과 예약 시각으로 방송을 만들고 campaignKey 를 받습니다.
  2. 미션 2 · 상품 등록 — 고객사 상품 ID(customerProductId)로 상품을 등록합니다.
  3. 미션 3 · 방송에 상품 연결 — 등록한 상품을 방송에 붙이고 순서를 정합니다.
  4. 미션 4 · 송출 정보 확인 — RTMP 주소·스트림 키·플레이어 URL 을 받습니다.
  5. 미션 5 · 내 페이지에 플레이어 노출 — 웹 SDK 코드 몇 줄로 방송을 띄웁니다.
  6. 미션 6 · (선택) API 로 시작·종료 — 콘솔 대신 자동화가 필요할 때.

이전 ← 규약다음 → 미션 1 · 방송 생성

미션 · 첫 방송 만들기

미션 1 · 방송 생성

방송(campaign)은 Shoplive 의 기본 단위입니다. 제목 하나만 있으면 만들 수 있고, 생성 즉시 플레이어 URL 과 송출 정보가 함께 발급됩니다.

POST/v2/{accessKey}/campaign레퍼런스 ↗
bash
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셀러·브랜드 소속으로 만들 때 지정합니다.
200 OK (예시, 일부 필드 생략)
{
  "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 · 상품 등록

상품은 고객사 단위로 한 번 등록해 두고 여러 방송에서 재사용합니다. 배열로 한 번에 여러 개를 등록할 수 있습니다.

POST/v2/{accessKey}/product레퍼런스 ↗
bash
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셀러 소유 상품으로 만들 때 지정. 생략하면 고객사 소유.
200 OK (예시)
{
  "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 는 추가 방식입니다. 보낸 상품이 방송 상품 목록의 끝에 순서대로 추가됩니다. 이미 연결된 상품은 다시 추가되지 않습니다.

POST/v2/{accessKey}/campaign/{campaignKeyOrId}/product레퍼런스 ↗
bash
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연결할 상품. 한 요청 안에서는 한 종류만 사용하세요.
200 OK (예시 · SKU-1002 는 이미 연결된 경우)
{
  "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 로 정합니다. 본문에 나열한 순서대로 정렬되고, 나열하지 않은 상품은 뒤로 밀립니다.

PUT/v2/{accessKey}/campaign/{campaignKeyOrId}/product/order레퍼런스 ↗
bash
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, 연결된 상품 목록이 모두 내려옵니다.

GET/v2/{accessKey}/campaign/{campaignKeyOrId}레퍼런스 ↗
bash
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY" \
  -H "Authorization: Bearer YOUR_JWT"
경로용도
stream.streamUrl + stream.streamKeyOBS 등 송출 프로그램의 서버 주소 · 스트림 키에 입력합니다. 송출을 켜면 콘솔 미리보기에 영상이 보입니다.
campaignMeta.shoplivePlayerUrl바로 열어볼 수 있는 시청 페이지.
campaignMeta.campaignKey다음 단계에서 웹 SDK 에 넘기는 값.
stream.liveUrlHLS(m3u8) 주소. 자체 플레이어를 쓸 때 사용합니다.
campaignProductList[]미션 3 에서 연결한 상품과 sortOrder.
송출과 방송 상태는 별개입니다 송출 프로그램이 영상을 보내는 것과 방송 상태를 ONAIR 로 바꾸는 것은 다른 일입니다. 보통 송출을 먼저 켜고 콘솔 미리보기로 확인한 뒤, 진행자가 콘솔에서 방송 시작을 누릅니다(API 로는 미션 6).

이전 ← 미션 3 · 방송에 상품 연결다음 → 미션 5 · 내 페이지에 플레이어 노출

미션 · 첫 방송 만들기

미션 5 · 내 페이지에 플레이어 노출

API 로 만든 방송은 웹 SDK 코드 몇 줄로 내 페이지에 띄울 수 있습니다. 이 단계까지 오면 첫 방송 만들기가 완료됩니다. 방송이 READY 상태면 예정 화면이, ONAIR 면 라이브가 보입니다.

html
<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 를 사용합니다. 송출 프로그램이 영상을 보내고 있는 상태에서 시작을 호출하면 시청자에게 라이브가 노출됩니다.

POST/v2/{accessKey}/console/{campaignKeyOrId}/start?rehearsal=true레퍼런스 ↗
POST/v2/{accessKey}/console/{campaignKeyOrId}/end레퍼런스 ↗
bash
# 리허설 시작 (입장 코드를 아는 사람만 시청)
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 를 호출해 라이브를 시작하세요.

영상 없이 start 만 호출하면 상태는 ONAIR 가 되지만 시청자에게는 대기 화면이 보입니다. 자동화한다면 송출 프로그램이 스트림을 보내고 있는지(콘솔 미리보기 또는 stream.liveUrl 응답 확인) 먼저 점검하세요.

이전 ← 미션 5 · 내 페이지에 플레이어 노출다음 → 상품 카탈로그 동기화

미션

상품 카탈로그 동기화

자사 상품 DB 가 원본이고 Shoplive 는 그 사본이어야 합니다. 상품 카탈로그 동기화는 customerProductId 를 축으로 상품을 만들고·고치고·지우는 동기화 루틴을 만들고, 방송 중 품절·재고 변화를 시청자 화면에 즉시 반영하는 방법을 다룹니다.

완료 조건자사 상품 변경이 배치 한 번으로 Shoplive 에 반영되고, 방송 중 품절이 플레이어에 바로 표시된다
사용하는 APIProduct · Campaign product · Console · Seller
전제첫 방송 만들기에서 만든 방송과 상품 2개

이전 ← 미션 6 · (선택) API 로 시작·종료다음 → 미션 7 · 상품 upsert 패턴

미션 · 상품 카탈로그 동기화

미션 7 · 상품 upsert 패턴

상품 등록(POST)은 같은 customerProductId 가 이미 있으면 그 항목을 status: FAIL 로 돌려줍니다. 수정(PUT)은 customerProductId 로 대상을 찾습니다. 두 가지를 조합하면 동기화 루틴이 됩니다.

  1. 변경된 상품을 모두 POST 로 보냅니다. 신규는 CREATE, 기존은 FAIL 로 나뉩니다.
  2. FAIL 로 돌아온 항목을 모아 PUT 으로 다시 보냅니다. 내용이 같아도 UPDATE 로 응답합니다. message 문구는 언어 설정에 따라 달라지니 문자열로 판별하지 마세요.
  3. 자사에서 삭제된 상품은 DELETE 로 정리합니다. 방송에 연결된 상품까지 지우지 않으려면 ?force=false 를 붙이세요. 이때 방송에 연결된 상품은 삭제되지 않고 FAIL 로 돌아옵니다. 기본값(true)은 상품을 방송의 상품 목록에서 빼고 삭제합니다.
PUT/v2/{accessKey}/product레퍼런스 ↗
bash
# 가격 인하 반영: 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 }
  ]'
DELETE/v2/{accessKey}/product?force=false레퍼런스 ↗
bash
# 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목록을 비우고 다시 구성할 때.
bash
# 방송에서 상품 하나 제외
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 는 특정 방송 안에서의 표시 상태만 다룹니다.

PUT/v2/{accessKey}/console/{campaignKeyOrId}/product/stockStatus레퍼런스 ↗
PUT/v2/{accessKey}/console/{campaignKeyOrId}/product/show레퍼런스 ↗
bash
# 품절 처리
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품절. 구매 버튼이 비활성화됩니다.
어디에 연결하나 자사 재고 시스템의 품절 이벤트를 받아 이 API 를 호출하는 작은 워커를 두면, 진행자가 콘솔에서 손으로 품절을 누르지 않아도 됩니다. 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 을 자사 페이지에 연결합니다. 핵심은 폴링 대신 웹훅으로 종료를 감지하는 것입니다.

완료 조건방송 종료 후 자동으로 통계 요약과 채팅·상품 클릭 로그가 자사 DB 에 적재되고, 다시보기 링크가 게시된다
사용하는 APIWebhook · Stats · Log · Campaign
전제종료된 방송 1개 (첫 방송 만들기에서 만든 방송을 끝내도 됩니다)

이전 ← 미션 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));
}

종료된 방송을 주기적으로 조회하면 웹훅을 아직 등록하지 못한 동안의 대안이 되고, 등록한 뒤에도 놓친 이벤트를 메우는 보정 작업으로 쓸 수 있습니다. 몇 분 간격이면 충분합니다.

bash (폴링 · 보정)
# 최근 하루 동안 예약됐고 지금 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 를 사용합니다.

GET/v2/{accessKey}/campaign/{campaignKeyOrId}/stats레퍼런스 ↗
bash
curl -s "https://private.shopliveapi.com/v2/YOUR_ACCESS_KEY/campaign/CAMPAIGN_KEY/stats" \
  -H "Authorization: Bearer YOUR_JWT"

통계는 방송이 종료된 뒤에 조회할 수 있습니다. 진행 중이거나 종료 직후 집계 전에는 404 로 응답합니다.

200 OK (예시 · 일부 필드 생략)
{
  "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 는 해당 행동을 한 고유 시청자 수.
GET/v2/{accessKey}/campaign/stats?from=…&to=…&campaignKeys=…레퍼런스 ↗
bash
# 지난 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주일)로 지정합니다.

GET/v2/{accessKey}/log?type=…&format=…&from=…&to=…&ck=…레퍼런스 ↗
bash
# 방송 하나의 상품 클릭 로그를 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
파라미터값
typelikeLog · chatLog · productClickLog · popupClickLog · viewerAccessLog · viewerActivityLog · highlightedChatLog · filteredChatLog
formatjson · csv · tsv
from, toISO 8601. ck 없이 조회할 때 필수이며 범위는 최대 1주일.
ck방송 키 또는 ID 로 한정(선택). 지정하면 from/to 대신 방송 시작 시각부터 4일 후까지의 범위가 적용됩니다. 시작된 방송만 지정할 수 있습니다.
excludeAdmin기본 true. viewerAccessLog·viewerActivityLog 에서 관리자·운영자의 활동을 제외합니다. 채팅은 필터링하지 않습니다.
시청자 식별 로그의 사용자 식별 컬럼은 SDK 에서 사용자 정보를 연결한 시청자만 채워집니다. 자사 회원과 매칭하려면 웹 SDK 가이드의 사용자 정보 연결 단계를 먼저 적용하세요.

이전 ← 미션 12 · 통계 조회다음 → 미션 14 · 다시보기 URL

미션 · 방송 결과 수집

미션 14 · 다시보기 URL

방송 생성 시 serveReplay: true 였다면 종료 후 다시보기가 자동으로 만들어집니다. 첫 방송 만들기의 웹 SDK 코드는 종료된 방송에서 그대로 다시보기를 재생하므로 별도 작업이 없어도 됩니다. 자체 플레이어나 다른 채널에 쓰려면 주소를 꺼내옵니다.

bash
# 종료된 방송의 다시보기 주소
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 입니다.

종료 후 집계가 마무리되기까지 수 분이 걸릴 수 있습니다. 웹훅 종료 이벤트를 받으면 바로 조회하지 말고 몇 분 뒤로 예약하거나, 값이 바뀌지 않을 때까지 몇 차례 재조회하세요.

이 외에 궁금한 점이 있다면 요청 시각(UTC), 호출한 엔드포인트, 응답의 _s·_e 값을 함께 담당 매니저 또는 기술 지원 채널로 보내주세요. 토큰이나 secretKey 원문은 보내지 마세요.