아마존 SP API 통합 방법: 단계별 튜토리얼

Ashley Innocent

Ashley Innocent

20 March 2026

아마존 SP API 통합 방법: 단계별 튜토리얼

요약

아마존 셀링 파트너 API (SP-API)는 주문, 재고, 상품 목록 및 주문 처리에 대한 셀러 데이터에 프로그램 방식으로 액세스할 수 있도록 하는 REST 기반 API입니다. 이 API는 IAM 역할을 사용한 OAuth 2.0 인증을 사용하고, AWS SigV4 서명을 요구하며, 엔드포인트에 따라 달라지는 호출 속도 제한(초당 0.1~100회 요청)을 적용합니다. 이 가이드는 계정 설정, 인증, 핵심 엔드포인트, 웹훅 구독 및 프로덕션 배포 전략을 다룹니다.

소개

아마존은 전 세계 200개 이상의 마켓플레이스에서 3억 5천만 개 이상의 제품을 처리합니다. 전자상거래 도구, 재고 관리 시스템 또는 분석 플랫폼을 구축하는 개발자에게 아마존 SP-API 통합은 선택 사항이 아닌 필수 사항입니다.

현실은 이렇습니다. 아마존 운영을 관리하는 셀러는 주문, 재고 및 상품 목록 전반에 걸쳐 수동 데이터 입력에 매주 20-30시간을 낭비합니다. 견고한 SP-API 통합은 여러 마켓플레이스에서 주문 동기화, 재고 업데이트 및 상품 목록 관리를 자동화합니다.

이 가이드는 아마존 SP-API 통합의 전체 과정을 안내합니다. IAM 역할 설정, OAuth 2.0 인증, AWS SigV4 요청 서명, 주문 및 재고 관리, 알림 구독, 오류 문제 해결 방법을 배우게 될 것입니다. 마지막에는 프로덕션 환경에서 사용할 수 있는 아마존 통합을 완성하게 될 것입니다.

💡
Apidog는 API 통합 테스트를 간소화합니다. SP-API 엔드포인트를 테스트하고, OAuth 흐름을 검증하고, 요청 서명을 검사하고, 인증 문제를 한 작업 공간에서 디버그하세요. API 사양을 가져오고, 응답을 모의하고, 테스트 시나리오를 팀과 공유하세요.
button

아마존 SP-API란 무엇인가요?

아마존 셀링 파트너 API (SP-API)는 셀러 센트럴 데이터에 프로그래밍 방식으로 액세스할 수 있도록 하는 REST 기반 API입니다. 이 API는 향상된 보안, 성능 및 기능을 통해 기존의 Marketplace Web Service (MWS)를 대체했습니다.

주요 기능

SP-API는 다음을 처리합니다.

SP-API 대 MWS 비교

기능 SP-API MWS (레거시)
아키텍처 RESTful JSON XML 기반
인증 OAuth 2.0 + IAM MWS 인증 토큰
보안 AWS SigV4 서명 단순 토큰
호출 속도 제한 엔드포인트별 동적 고정 할당량
마켓플레이스 통합 엔드포인트 지역별
상태 현재 사용 중단 예정 (2025년 12월)

모든 MWS 통합을 즉시 SP-API로 마이그레이션하세요. 아마존은 2025년 12월에 MWS를 완전히 중단한다고 발표했습니다.

API 아키텍처 개요

아마존은 중앙 집중식 인증을 사용하는 지역별 API 구조를 사용합니다.

https://sellingpartnerapi-na.amazon.com (북미)
https://sellingpartnerapi-eu.amazon.com (유럽)
https://sellingpartnerapi-fe.amazon.com (극동)

모든 요청은 다음을 필요로 합니다.

  1. AWS SigV4 서명
  2. OAuth 흐름을 통한 액세스 토큰
  3. 적절한 IAM 역할 권한
  4. 추적을 위한 요청 ID

지원되는 마켓플레이스

지역 마켓플레이스 API 엔드포인트
북미 미국, 캐나다, 멕시코 sellingpartnerapi-na.amazon.com
유럽 영국, 독일, 프랑스, 이탈리아, 스페인, 네덜란드, 스웨덴, 폴란드, 터키, 이집트, 인도, 아랍에미리트, 사우디아라비아 sellingpartnerapi-eu.amazon.com
극동 일본, 호주, 싱가포르, 브라질 sellingpartnerapi-fe.amazon.com

시작하기: 계정 및 IAM 설정

1단계: 아마존 개발자 계정 생성

SP-API에 액세스하기 전에 적절한 계정 액세스 권한이 필요합니다.

  1. 아마존 개발자 센터 방문
  2. 아마존 계정으로 로그인 (셀러 센트럴 액세스 권한이 있어야 함)
  3. 대시보드에서 Selling Partner API로 이동
  4. 개발자 계약 동의

2단계: 애플리케이션 등록

셀러 센트럴에 애플리케이션 프로필을 생성합니다.

  1. 셀러 센트럴에 로그인
  2. 앱 및 서비스 > 앱 개발로 이동
  3. 새 앱 추가 클릭
  4. 애플리케이션 세부 정보 입력:

제출 후 다음을 받게 됩니다.

보안 참고: 자격 증명은 환경 변수에 저장하고, 코드에 절대 포함하지 마세요.

# .env 파일
AMAZON_APPLICATION_ID="amzn1.application.xxxxx"
AMAZON_CLIENT_ID="amzn1.account.xxxxx"
AMAZON_CLIENT_SECRET="your_client_secret_here"
AMAZON_SELLER_ID="your_seller_id_here"
AWS_ACCESS_KEY_ID="your_aws_access_key"
AWS_SECRET_ACCESS_KEY="your_aws_secret_key"
AWS_REGION="us-east-1"

3단계: SP-API용 IAM 역할 생성

SP-API는 특정 권한을 가진 IAM 역할을 필요로 합니다.

  1. AWS IAM 콘솔에 로그인
  2. 역할 > 역할 생성으로 이동
  3. 신뢰할 수 있는 엔터티로 다른 AWS 계정 선택
  4. 해당 지역의 아마존 계정 ID 입력:

4단계: IAM 정책 구성

이 정책을 IAM 역할에 연결하세요.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "execute-api:Invoke"
      ],
      "Resource": [
        "arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/*"
      ]
    }
  ]
}

역할 이름을 SellingPartnerApiRole과 같이 설명적으로 지정하고 ARN을 기록해 두세요.

5단계: IAM 역할을 애플리케이션에 연결

IAM 역할을 SP-API 애플리케이션에 연결하세요.

  1. 셀러 센트럴 > 앱 개발로 돌아가기
  2. 애플리케이션 선택
  3. 편집 > IAM 역할 ARN 클릭
  4. IAM 역할 ARN 입력
  5. 변경 사항 저장

아마존은 몇 분 안에 IAM 역할을 검증합니다. 준비가 되면 "연결됨" 상태를 볼 수 있습니다.

OAuth 2.0 인증 흐름

SP-API OAuth 이해하기

아마존은 OAuth 2.0을 인증에 사용합니다. 다음은 전체 흐름입니다.

1. 셀러가 애플리케이션에서 "인증" 클릭
2. 앱이 아마존 인증 URL로 리다이렉션
3. 셀러가 로그인하고 권한 부여
4. 아마존이 인증 코드를 가지고 다시 리다이렉션
5. 앱이 코드를 LWA (로그인 위드 아마존) 토큰으로 교환
6. 앱이 LWA 토큰을 SP-API 액세스 토큰으로 교환
7. 앱이 액세스 토큰을 사용하여 API 호출 (SigV4 서명됨)
8. 액세스 토큰 만료 시 (1시간) 토큰 새로 고침

6단계: 인증 URL 생성

OAuth 인증 URL을 생성합니다.

const generateAuthUrl = (clientId, redirectUri, state) => {
  const baseUrl = 'https://www.amazon.com/sp/apps/oauth/authorize';
  const params = new URLSearchParams({
    application_id: process.env.AMAZON_APPLICATION_ID,
    client_id: clientId,
    redirect_uri: redirectUri,
    state: state, // CSRF 보호를 위한 임의 문자열
    scope: 'sellingpartnerapi::notifications'
  });

  return `${baseUrl}?${params.toString()}`;
};

// 사용 예시
const authUrl = generateAuthUrl(
  process.env.AMAZON_CLIENT_ID,
  'https://your-app.com/callback',
  crypto.randomBytes(16).toString('hex')
);

console.log(`사용자를 다음으로 리다이렉션: ${authUrl}`);

필요한 OAuth 범위

애플리케이션에 필요한 권한만 요청하세요.

범위 설명 사용 사례
sellingpartnerapi::notifications 알림 수신 웹훅 구독
sellingpartnerapi::migration MWS에서 마이그레이션 레거시 통합

대부분의 API 액세스는 OAuth 범위가 아닌 IAM 정책에 의해 제어됩니다.

7단계: 코드를 LWA 토큰으로 교환

OAuth 콜백을 처리하고 인증 코드를 교환합니다.

const exchangeCodeForLwaToken = async (code, redirectUri) => {
  const response = await fetch('https://api.amazon.com/auth/o2/token', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: process.env.AMAZON_CLIENT_ID,
      client_secret: process.env.AMAZON_CLIENT_SECRET,
      redirect_uri: redirectUri,
      code: code
    })
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`LWA Token Error: ${error.error_description}`);
  }

  const data = await response.json();

  return {
    access_token: data.access_token,
    refresh_token: data.refresh_token,
    expires_in: data.expires_in, // 일반적으로 3600초 (1시간)
    token_type: data.token_type
  };
};

// 콜백 경로 처리
app.get('/callback', async (req, res) => {
  const { spapi_oauth_code, state } = req.query;

  // 보낸 state와 일치하는지 확인 (CSRF 보호)
  if (state !== req.session.oauthState) {
    return res.status(400).send('Invalid state parameter');
  }

  try {
    const tokens = await exchangeCodeForLwaToken(spapi_oauth_code, 'https://your-app.com/callback');

    // 셀러와 연결된 데이터베이스에 토큰 저장
    await db.sellers.update(req.session.sellerId, {
      amazon_lwa_access_token: tokens.access_token,
      amazon_lwa_refresh_token: tokens.refresh_token,
      amazon_token_expires: Date.now() + (tokens.expires_in * 1000)
    });

    res.redirect('/dashboard');
  } catch (error) {
    console.error('Token exchange failed:', error);
    res.status(500).send('Authentication failed');
  }
});

8단계: LWA 토큰을 SP-API 자격 증명으로 교환

LWA 액세스 토큰을 사용하여 임시 AWS 자격 증명을 가져옵니다.

const assumeRole = async (lwaAccessToken) => {
  const response = await fetch('https://api.amazon.com/auth/o2/token', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.AMAZON_CLIENT_ID,
      client_secret: process.env.AMAZON_CLIENT_SECRET,
      scope: 'sellingpartnerapi::notifications'
    })
  });

  const data = await response.json();

  // STS를 통해 AWS 자격 증명으로 교환
  const stsResponse = await fetch('https://sts.amazonaws.com/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Authorization': `Bearer ${data.access_token}`
    },
    body: new URLSearchParams({
      Action: 'AssumeRole',
      RoleArn: 'arn:aws:iam::YOUR_ACCOUNT:role/SellingPartnerApiRole',
      RoleSessionName: 'sp-api-session',
      Version: '2011-06-15'
    })
  });

  return stsResponse;
};

9단계: 토큰 새로 고침 구현

액세스 토큰은 1시간 후에 만료됩니다. 자동 새로 고침을 구현하세요.

const refreshLwaToken = async (refreshToken) => {
  const response = await fetch('https://api.amazon.com/auth/o2/token', {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/x-www-form-urlencoded'
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      client_id: process.env.AMAZON_CLIENT_ID,
      client_secret: process.env.AMAZON_CLIENT_SECRET,
      refresh_token: refreshToken
    })
  });

  const data = await response.json();

  return {
    access_token: data.access_token,
    refresh_token: data.refresh_token, // 항상 새 새로 고침 토큰 저장
    expires_in: data.expires_in
  };
};

// 유효한 토큰을 보장하는 미들웨어
const ensureValidToken = async (sellerId) => {
  const seller = await db.sellers.findById(sellerId);

  // 토큰이 5분 이내에 만료되는지 확인
  if (seller.amazon_token_expires < Date.now() + 300000) {
    const newTokens = await refreshLwaToken(seller.amazon_lwa_refresh_token);

    await db.sellers.update(sellerId, {
      amazon_lwa_access_token: newTokens.access_token,
      amazon_lwa_refresh_token: newTokens.refresh_token,
      amazon_token_expires: Date.now() + (newTokens.expires_in * 1000)
    });

    return newTokens.access_token;
  }

  return seller.amazon_lwa_access_token;
};

AWS SigV4 요청 서명

SigV4 이해하기

모든 SP-API 요청은 AWS Signature Version 4 (SigV4) 서명을 필요로 합니다. 이는 요청의 신뢰성과 무결성을 보장합니다.

SigV4 서명 프로세스

const crypto = require('crypto');

class SigV4Signer {
  constructor(accessKey, secretKey, region, service = 'execute-api') {
    this.accessKey = accessKey;
    this.secretKey = secretKey;
    this.region = region;
    this.service = service;
  }

  sign(method, url, body = '', headers = {}) {
    const parsedUrl = new URL(url);
    const now = new Date();

    // 1단계: 정식 요청 생성
    const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '');
    const dateStamp = amzDate.slice(0, 8);

    headers['host'] = parsedUrl.host;
    headers['x-amz-date'] = amzDate;
    headers['x-amz-access-token'] = this.accessToken;
    headers['content-type'] = 'application/json';

    const canonicalHeaders = Object.entries(headers)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}`)
      .join('\n');

    const signedHeaders = Object.keys(headers)
      .sort()
      .map(k => k.toLowerCase())
      .join(';');

    const payloadHash = crypto.createHash('sha256').update(body).digest('hex');

    const canonicalRequest = [
      method.toUpperCase(),
      parsedUrl.pathname,
      parsedUrl.search.slice(1),
      canonicalHeaders,
      '',
      signedHeaders,
      payloadHash
    ].join('\n');

    // 2단계: 서명할 문자열 생성
    const algorithm = 'AWS4-HMAC-SHA256';
    const credentialScope = `${dateStamp}/${this.region}/${this.service}/aws4_request`;

    const stringToSign = [
      algorithm,
      amzDate,
      credentialScope,
      crypto.createHash('sha256').update(canonicalRequest).digest('hex')
    ].join('\n');

    // 3단계: 서명 계산
    const kDate = this.hmac(`AWS4${this.secretKey}`, dateStamp);
    const kRegion = this.hmac(kDate, this.region);
    const kService = this.hmac(kRegion, this.service);
    const kSigning = this.hmac(kService, 'aws4_request');
    const signature = this.hmac(kSigning, stringToSign, 'hex');

    // 4단계: 인증 헤더 추가
    const authorization = `${algorithm} Credential=${this.accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;

    return {
      headers: {
        ...headers,
        'Authorization': authorization
      },
      canonicalRequest,
      stringToSign,
      signature
    };
  }

  hmac(key, data, encoding = 'buffer') {
    return crypto.createHmac('sha256', key).update(data).digest(encoding);
  }
}

// 사용 예시
const signer = new SigV4Signer(
  process.env.AWS_ACCESS_KEY_ID,
  process.env.AWS_SECRET_ACCESS_KEY,
  'us-east-1'
);

const signedRequest = signer.sign('GET', 'https://sellingpartnerapi-na.amazon.com/orders/v0/orders', '', {
  'x-amz-access-token': accessToken
});

SigV4에 AWS SDK 사용

AWS SDK로 서명을 간소화합니다.

const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { Sha256 } = require('@aws-crypto/sha256-js');

const signer = new SignatureV4({
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
  },
  region: 'us-east-1',
  service: 'execute-api',
  sha256: Sha256
});

const makeSpApiRequest = async (method, endpoint, accessToken, body = null) => {
  const url = new URL(endpoint);

  const headers = {
    'host': url.host,
    'content-type': 'application/json',
    'x-amz-access-token': accessToken,
    'x-amz-date': new Date().toISOString().replace(/[:-]|\.\d{3}/g, '')
  };

  const signedRequest = await signer.sign({
    method,
    hostname: url.hostname,
    path: url.pathname,
    query: Object.fromEntries(url.searchParams),
    headers,
    body: body ? JSON.stringify(body) : undefined
  });

  const response = await fetch(endpoint, {
    method,
    headers: signedRequest.headers,
    body: signedRequest.body
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`SP-API Error: ${error.errors?.[0]?.message || response.statusText}`);
  }

  return response.json();
};

주문 API

주문 검색

필터링 옵션으로 주문을 가져옵니다.

const getOrders = async (accessToken, options = {}) => {
  const params = new URLSearchParams({
    createdAfter: options.createdAfter, // ISO 8601 형식
    createdBefore: options.createdBefore,
    orderStatuses: options.orderStatuses?.join(',') || '',
    marketplaceIds: options.marketplaceIds?.join(',') || ['ATVPDKIKX0DER'], // 미국
    maxResultsPerPage: options.maxResultsPerPage || 100
  });

  // 비어있는 파라미터 제거
  for (const [key, value] of params.entries()) {
    if (!value) params.delete(key);
  }

  const endpoint = `https://sellingpartnerapi-na.amazon.com/orders/v0/orders?${params.toString()}`;

  return makeSpApiRequest('GET', endpoint, accessToken);
};

// 사용 예시
const orders = await getOrders(accessToken, {
  createdAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 지난 24시간
  orderStatuses: ['Unshipped', 'PartiallyShipped'],
  marketplaceIds: ['ATVPDKIKX0DER'] // 미국 마켓플레이스
});

주문 응답 구조

{
  "payload": {
    "orders": [
      {
        "amazon_order_id": "112-1234567-1234567",
        "seller_order_id": "ORDER-001",
        "purchase_date": "2026-03-19T10:30:00Z",
        "last_update_date": "2026-03-19T14:45:00Z",
        "order_status": "Unshipped",
        "fulfillment_channel": "AFN", // AFN = FBA, MFN = 셀러 직접 배송
        "sales_channel": "Amazon.com",
        "order_channel": "Amazon.com",
        "ship_service_level": "Std US D2D Dom",
        "order_total": {
          "currency_code": "USD",
          "amount": "89.99"
        },
        "number_of_items_shipped": 0,
        "number_of_items_unshipped": 2,
        "payment_execution_detail": [],
        "payment_method": "CreditCard",
        "payment_method_details": ["CreditCard"],
        "marketplace_id": "ATVPDKIKX0DER",
        "shipment_service_level_category": "Standard",
        "easy_ship_shipment_status": null,
        "is_business_order": false,
        "is_prime": true,
        "is_premium_order": false,
        "is_global_express_enabled": false
      }
    ],
    "next_token": "eyJleHBpcmF0aW9uVGltZU9mTmV4dFRva2VuIjoxNzEwOTUwNDAwfQ=="
  }
}

주문 항목 가져오기

주문에 대한 상세 품목을 검색합니다.

const getOrderItems = async (accessToken, orderId) => {
  const endpoint = `https://sellingpartnerapi-na.amazon.com/orders/v0/orders/${orderId}/orderItems`;

  return makeSpApiRequest('GET', endpoint, accessToken);
};

// 사용 예시
const orderItems = await getOrderItems(accessToken, '112-1234567-1234567');

// 예상 응답
{
  "payload": {
    "order_items": [
      {
        "asin": "B08N5WRWNW",
        "seller_sku": "MYSKU-001",
        "title": "Wireless Bluetooth Headphones",
        "quantity_ordered": 2,
        "quantity_shipped": 0,
        "product_info": {
          "number_of_items": 2
        },
        "item_price": {
          "currency_code": "USD",
          "amount": "44.99"
        },
        "item_total": {
          "currency_code": "USD",
          "amount": "89.98"
        },
        "tax_collection": {
          "tax_collection_model": "MarketplaceFacilitator",
          "responsible_party": "Amazon Services, Inc."
        }
      }
    ]
  }
}

배송 상태 업데이트

추적 정보로 주문을 배송 완료로 표시합니다.

const confirmShipment = async (accessToken, orderId, shipmentData) => {
  const endpoint = `https://sellingpartnerapi-na.amazon.com/orders/v0/orders/${orderId}/shipmentConfirmation`;

  const payload = {
    packageDetails: {
      packageReferenceId: shipmentData.packageReferenceId || '1',
      carrier_code: shipmentData.carrierCode, // 예: 'USPS', 'FEDEX', 'UPS'
      tracking_number: shipmentData.trackingNumber,
      ship_date: shipmentData.shipDate || new Date().toISOString(),
      items: shipmentData.items.map(item => ({
        order_item_id: item.orderItemId,
        quantity: item.quantity
      }))
    }
  };

  return makeSpApiRequest('POST', endpoint, accessToken, payload);
};

// 사용 예시
await confirmShipment(accessToken, '112-1234567-1234567', {
  carrierCode: 'USPS',
  trackingNumber: '9400111899223456789012',
  items: [
    { orderItemId: '12345678901234', quantity: 2 }
  ]
});

일반 택배사 코드

택배사 택배사 코드
USPS USPS
FedEx FEDEX
UPS UPS
DHL DHL
캐나다 포스트 CANADA_POST
로얄 메일 ROYAL_MAIL
오스트레일리아 포스트 AUSTRALIA_POST
아마존 물류 AMZN_UK

재고 API

재고 요약 가져오기

여러 마켓플레이스 전반의 재고 수준을 가져옵니다.

const getInventorySummaries = async (accessToken, options = {}) => {
  const params = new URLSearchParams({
    granularityType: options.granularityType || 'Marketplace',
    granularityId: options.granularityId || 'ATVPDKIKX0DER', // 미국
    startDateTime: options.startDateTime || '',
    sellerSkus: options.sellerSkus?.join(',') || ''
  });

  const endpoint = `https://sellingpartnerapi-na.amazon.com/fba/inventory/v1/summaries?${params.toString()}`;

  return makeSpApiRequest('GET', endpoint, accessToken);
};

// 사용 예시
const inventory = await getInventorySummaries(accessToken, {
  granularityId: 'ATVPDKIKX0DER',
  sellerSkus: ['MYSKU-001', 'MYSKU-002']
});

재고 응답 구조

{
  "payload": {
    "inventorySummaries": [
      {
        "asin": "B08N5WRWNW",
        "seller_sku": "MYSKU-001",
        "condition": "NewItem",
        "details": {
          "quantity": 150,
          "fulfillable_quantity": 145,
          "inbound_working_quantity": 0,
          "inbound_shipped_quantity": 5,
          "inbound_receiving_quantity": 0,
          "reserved_quantity": 5,
          "unfulfillable_quantity": 0,
          "warehouse_damage_quantity": 0,
          "distributor_damaged_quantity": 0,
          "carrier_damaged_quantity": 0,
          "defective_quantity": 0,
          "customer_damaged_quantity": 0
        },
        "marketplace_id": "ATVPDKIKX0DER"
      }
    ]
  }
}

재고 업데이트

참고: SP-API는 직접적인 재고 업데이트 엔드포인트를 제공하지 않습니다. 재고는 다음을 통해 관리됩니다.

  1. FBA 배송 - 아마존 창고로 재고 발송
  2. MFN 주문 - 주문 배송 시 재고 자동 감소
  3. 상품 목록 업데이트 - 상품 목록 API를 통해 수량 조정

FBA의 경우, 배송 계획을 생성합니다.

const createInboundShipmentPlan = async (accessToken, shipmentData) => {
  const endpoint = 'https://sellingpartnerapi-na.amazon.com/fba/inbound/v0/plans';

  const payload = {
    ShipFromAddress: {
      Name: shipmentData.shipFromName,
      AddressLine1: shipmentData.shipFromAddress,
      City: shipmentData.shipFromCity,
      StateOrProvinceCode: shipmentData.shipFromState,
      CountryCode: shipmentData.shipFromCountry,
      PostalCode: shipmentData.shipFromPostalCode
    },
    LabelPrepPreference: 'SELLER_LABEL',
    InboundPlanItems: shipmentData.items.map(item => ({
      SellerSKU: item.sku,
      ASIN: item.asin,
      Quantity: item.quantity,
      Condition: 'NewItem'
    }))
  };

  return makeSpApiRequest('POST', endpoint, accessToken, payload);
};

상품 목록 API

상품 목록 가져오기

필터링으로 제품 상품 목록을 가져옵니다.

const getListings = async (accessToken, options = {}) => {
  const params = new URLSearchParams({
    marketplaceIds: options.marketplaceIds?.join(',') || ['ATVPDKIKX0DER'],
    itemTypes: options.itemTypes?.join(',') || ['ASIN', 'SKU'],
    identifiers: options.identifiers?.join(',') || '',
    issuesLocale: options.locale || 'en_US'
  });

  const endpoint = `https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items?${params.toString()}`;

  return makeSpApiRequest('GET', endpoint, accessToken);
};

// 사용 예시
const listings = await getListings(accessToken, {
  identifiers: ['B08N5WRWNW', 'B09JQKJXYZ'],
  itemTypes: ['ASIN']
});

상품 목록 응답 구조

{
  "identifiers": {
    "marketplaceId": "ATVPDKIKX0DER",
    "sku": "MYSKU-001",
    "asin": "B08N5WRWNW"
  },
  "attributes": {
    "title": "Wireless Bluetooth Headphones",
    "description": "Premium wireless headphones with noise cancellation",
    "brand": "MyBrand",
    "color": "Black",
    "size": "One Size",
    "item_weight": "0.5 pounds",
    "product_dimensions": "7 x 6 x 3 inches"
  },
  "product_type": "LUGGAGE",
  "sales_price": {
    "currency_code": "USD",
    "amount": "89.99"
  },
  "list_price": {
    "currency_code": "USD",
    "amount": "129.99"
  },
  "fulfillment_availability": [
    {
      "fulfillment_channel_code": "AFN",
      "quantity": 150
    }
  ],
  "condition_type": "New",
  "status": "ACTIVE",
  "procurement": null
}

상품 목록 생성 또는 업데이트

일괄 작업을 위해 `submitListingsSubmission`을 사용합니다.

const submitListingUpdate = async (accessToken, listingData) => {
  const endpoint = 'https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items/MYSKU-001';

  const payload = {
    productType: 'LUGGAGE',
    patches: [
      {
        op: 'replace',
        path: '/attributes/title',
        value: 'Updated Wireless Bluetooth Headphones - Premium Sound'
      },
      {
        op: 'replace',
        path: '/salesPrice',
        value: {
          currencyCode: 'USD',
          amount: '79.99'
        }
      }
    ]
  };

  return makeSpApiRequest('PATCH', endpoint, accessToken, payload);
};

상품 목록 삭제

상품 목록을 제거하거나 비활성화합니다.

const deleteListing = async (accessToken, sku, marketplaceIds) => {
  const params = new URLSearchParams({
    marketplaceIds: marketplaceIds.join(',')
  });

  const endpoint = `https://sellingpartnerapi-na.amazon.com/listings/2021-08-01/items/${sku}?${params.toString()}`;

  return makeSpApiRequest('DELETE', endpoint, accessToken);
};

보고서 API

보고서 스케줄 생성

보고서 생성을 자동화합니다.

const createReport = async (accessToken, reportType, dateRange) => {
  const endpoint = 'https://sellingpartnerapi-na.amazon.com/reports/2021-06-30/reports';

  const payload = {
    reportType: reportType,
    marketplaceIds: dateRange.marketplaceIds || ['ATVPDKIKX0DER'],
    dataStartTime: dateRange.startTime?.toISOString(),
    dataEndTime: dateRange.endTime?.toISOString()
  };

  return makeSpApiRequest('POST', endpoint, accessToken, payload);
};

// 일반적인 보고서 유형
const REPORT_TYPES = {
  ORDERS: 'GET_FLAT_FILE_ALL_ORDERS_DATA_BY_LAST_UPDATE_GENERAL',
  ORDER_ITEMS: 'GET_FLAT_FILE_ORDER_ITEMS_DATA_BY_LAST_UPDATE_GENERAL',
  INVENTORY: 'GET_MERCHANT_LISTINGS_ALL_DATA',
  FBA_INVENTORY: 'GET_FBA_MYI_UNSUPPRESSED_INVENTORY_DATA',
  SETTLEMENT: 'GET_V2_SETTLEMENT_REPORT_DATA_FLAT_FILE',
  SALES_AND_TRAFFIC: 'GET_SALES_AND_TRAFFIC_REPORT',
  ADVERTISING: 'GET_BRAND_ANALYTICS_SEARCH_TERMS_REPORT'
};

// 사용 예시
const report = await createReport(accessToken, REPORT_TYPES.ORDERS, {
  marketplaceIds: ['ATVPDKIKX0DER'],
  startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // 지난 7일
  endTime: new Date()
});

보고서 문서 가져오기

생성된 보고서를 다운로드합니다.

const getReportDocument = async (accessToken, reportId) => {
  const endpoint = `https://sellingpartnerapi-na.amazon.com/reports/2021-06-30/reports/${reportId}/document`;

  return makeSpApiRequest('GET', endpoint, accessToken);
};

// 보고서 다운로드 및 파싱
const downloadReport = async (accessToken, reportId) => {
  const documentInfo = await getReportDocument(accessToken, reportId);

  const response = await fetch(documentInfo.payload.url);
  const content = await response.text();

  // 보고서는 일반적으로 탭으로 구분되거나 JSON 형식
  if (documentInfo.payload.compressionAlgorithm === 'GZIP') {
    const decompressed = await decompressGzip(content);
    return decompressed;
  }

  return content;
};

알림 API

구독 생성

실시간 이벤트를 위한 웹훅을 설정합니다.

const createSubscription = async (accessToken, subscriptionData) => {
  const endpoint = 'https://sellingpartnerapi-na.amazon.com/notifications/v1/subscriptions';

  const payload = {
    payload: {
      destination: {
        resource: subscriptionData.destinationArn, // SNS 주제 ARN
        name: subscriptionData.name
      },
      modelVersion: '1.0',
      eventFilter: {
        eventCode: subscriptionData.eventCode,
        marketplaceIds: subscriptionData.marketplaceIds
      }
    }
  };

  return makeSpApiRequest('POST', endpoint, accessToken, payload);
};

// 사용 가능한 이벤트 유형
const EVENT_CODES = {
  ORDER_STATUS_CHANGE: 'OrderStatusChange',
  ORDER_ITEM_CHANGE: 'OrderItemChange',
  ORDER_CHANGE: 'OrderChange',
  FBA_ORDER_STATUS_CHANGE: 'FBAOrderStatusChange',
  FBA_OUTBOUND_SHIPMENT_STATUS: 'FBAOutboundShipmentStatus',
  INVENTORY_LEVELS: 'InventoryLevels',
  PRICING_HEALTH: 'PricingHealth'
};

// 사용 예시
await createSubscription(accessToken, {
  destinationArn: 'arn:aws:sns:us-east-1:123456789012:sp-api-notifications',
  name: 'OrderStatusNotifications',
  eventCode: EVENT_CODES.ORDER_STATUS_CHANGE,
  marketplaceIds: ['ATVPDKIKX0DER']
});

SNS 대상 설정

아마존은 SNS 주제로 알림을 보냅니다.

const createSnsDestination = async (accessToken, destinationData) => {
  const endpoint = 'https://sellingpartnerapi-na.amazon.com/notifications/v1/destinations';

  const payload = {
    resource: destinationData.snsTopicArn,
    name: destinationData.name
  };

  return makeSpApiRequest('POST', endpoint, accessToken, { payload });
};

// SNS 주제 정책은 아마존 SES가 게시하도록 허용해야 합니다
const snsTopicPolicy = {
  Version: '2012-10-17',
  Statement: [
    {
      Effect: 'Allow',
      Principal: {
        Service: 'notifications.amazon.com'
      },
      Action: 'SNS:Publish',
      Resource: 'arn:aws:sns:us-east-1:123456789012:sp-api-notifications'
    }
  ]
};

알림 처리

알림을 수신하도록 SNS 엔드포인트를 설정합니다.

const express = require('express');
const crypto = require('crypto');
const app = express();

app.post('/webhooks/amazon', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['x-amz-sns-message-signature'];
  const payload = req.body;

  // SNS 메시지 서명 확인
  const isValid = await verifySnsSignature(payload, signature);

  if (!isValid) {
    console.error('Invalid SNS signature');
    return res.status(401).send('Unauthorized');
  }

  const message = JSON.parse(payload.toString());

  // 다양한 메시지 유형 처리
  switch (message.Type) {
    case 'SubscriptionConfirmation':
      // 구독 자동 확인
      await fetch(message.SubscribeURL);
      break;
    case 'Notification':
      const notification = JSON.parse(message.Message);
      await handleSpApiNotification(notification);
      break;
  }

  res.status(200).send('OK');
});

async function handleSpApiNotification(notification) {
  const { notificationType, payload } = notification;

  switch (notificationType) {
    case 'OrderStatusChange':
      await syncOrderStatus(payload.amazonOrderId);
      break;
    case 'OrderChange':
      await syncOrderDetails(payload.amazonOrderId);
      break;
    case 'InventoryLevels':
      await updateInventoryCache(payload);
      break;
  }
}

호출 속도 제한 및 할당량

호출 속도 제한 이해하기

SP-API는 엔드포인트별 동적 호출 속도 제한을 적용합니다.

엔드포인트 카테고리 호출 속도 제한 버스트 제한
주문 초당 10회 요청 20
주문 항목 초당 5회 요청 10
재고 초당 2회 요청 5
상품 목록 초당 10회 요청 20
보고서 초당 0.5회 요청 1
알림 초당 1회 요청 2
FBA 입고 초당 2회 요청 5

현재 제한을 확인하려면 응답에서 x-amzn-RateLimit-Limit 헤더를 확인하세요.

호출 속도 제한 처리 구현

재시도를 위해 지수 백오프를 사용합니다.

const makeRateLimitedRequest = async (method, endpoint, accessToken, body = null, maxRetries = 5) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const response = await makeSpApiRequest(method, endpoint, accessToken, body);

      // 호출 속도 제한 헤더 확인
      const rateLimit = response.headers.get('x-amzn-RateLimit-Limit');
      const retryAfter = response.headers.get('Retry-After');

      if (retryAfter) {
        console.warn(`호출 속도 제한. 다음 시간 이후 재시도: ${retryAfter} 초`);
      }

      return response;
    } catch (error) {
      if (error.message.includes('429') && attempt < maxRetries) {
        // retry-after 헤더 추출 또는 지수 백오프 사용
        const retryAfter = error.headers?.get('Retry-After') || Math.pow(2, attempt);
        console.log(`호출 속도 제한. ${retryAfter}초 후 재시도...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
      } else if (error.message.includes('503') && attempt < maxRetries) {
        // 서비스를 사용할 수 없음 - 지수 백오프
        const delay = Math.pow(2, attempt) * 1000;
        console.log(`서비스를 사용할 수 없습니다. ${delay}ms 후 재시도...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        throw error;
      }
    }
  }
};

요청 큐잉 구현

제한 내에서 유지하기 위해 큐를 구현합니다.

class RateLimitedQueue {
  constructor(rateLimit, burstLimit = null) {
    this.rateLimit = rateLimit; // 초당 요청
    this.burstLimit = burstLimit || rateLimit * 2;
    this.tokens = this.burstLimit;
    this.lastRefill = Date.now();
    this.queue = [];
    this.processing = false;
  }

  async add(requestFn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ requestFn, resolve, reject });
      this.process();
    });
  }

  refillTokens() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    const tokensToAdd = elapsed * this.rateLimit;

    this.tokens = Math.min(this.burstLimit, this.tokens + tokensToAdd);
    this.lastRefill = now;
  }

  async process() {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;

    while (this.queue.length > 0) {
      this.refillTokens();

      if (this.tokens < 1) {
        const waitTime = (1 / this.rateLimit) * 1000;
        await new Promise(r => setTimeout(r, waitTime));
        continue;
      }

      this.tokens--;
      const { requestFn, resolve, reject } = this.queue.shift();

      try {
        const result = await requestFn();
        resolve(result);
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

// 사용 예시 - 주문 API 큐 (초당 10회 요청)
const ordersQueue = new RateLimitedQueue(10, 20);
const orders = await ordersQueue.add(() => getOrders(accessToken, options));

보안 모범 사례

자격 증명 관리

소스 코드에 자격 증명을 하드코딩하지 마세요. 환경 변수 또는 시크릿 관리자를 사용하세요.

// 나쁜 예 - 절대 이렇게 하지 마세요
const AWS_ACCESS_KEY = 'AKIAIOSFODNN7EXAMPLE';
const AWS_SECRET = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';

// 좋은 예 - 환경 변수 사용
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY_ID;
const AWS_SECRET = process.env.AWS_SECRET_ACCESS_KEY;

// 더 좋은 예 - AWS 시크릿 매니저 또는 유사한 서비스 사용
const { SecretsManagerClient } = require('@aws-sdk/client-secrets-manager');
const secretsClient = new SecretsManagerClient({ region: 'us-east-1' });

const getCredentials = async () => {
  const response = await secretsClient.send({
    Name: 'prod/sp-api/credentials'
  });
  return JSON.parse(response.SecretString);
};

토큰 저장 요구 사항

SP-API는 토큰 저장에 대한 특정 보안 조치를 요구합니다.

  1. 정지 상태 암호화: 저장된 토큰에 AES-256 암호화 사용
  2. 전송 중 암호화: 항상 HTTPS/TLS 1.2 이상 사용
  3. 접근 제어: 특정 서비스 계정으로 토큰 액세스 제한
  4. 감사 로깅: 모든 토큰 액세스 및 새로 고침 이벤트 로깅
  5. 자동 로테이션: 만료 전에 토큰 새로 고침
const crypto = require('crypto');

class TokenStore {
  constructor(encryptionKey) {
    this.algorithm = 'aes-256-gcm';
    this.key = crypto.createHash('sha256').update(encryptionKey).digest('hex').slice(0, 32);
  }

  encrypt(token) {
    const iv = crypto.randomBytes(16);
    const cipher = crypto.createCipheriv(this.algorithm, Buffer.from(this.key), iv);
    let encrypted = cipher.update(token, 'utf8', 'hex');
    encrypted += cipher.final('hex');
    const authTag = cipher.getAuthTag().toString('hex');

    return {
      iv: iv.toString('hex'),
      encryptedData: encrypted,
      authTag
    };
  }

  decrypt(encryptedData) {
    const decipher = crypto.createDecipheriv(
      this.algorithm,
      Buffer.from(this.key),
      Buffer.from(encryptedData.iv, 'hex')
    );
    decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'hex'));
    let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
  }
}

IAM 최소 권한

애플리케이션에 필요한 권한만 부여하세요.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SPAPIOrdersAccess",
      "Effect": "Allow",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/orders/*"
    },
    {
      "Sid": "SPAPIInventoryAccess",
      "Effect": "Allow",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:*:*:*/prod/*/sellingpartnerapi/fba/inventory/*"
    }
  ]
}

프로덕션 환경에서 * 와일드카드를 사용하지 마세요. 특정 엔드포인트에 대한 권한 범위를 지정하세요.

요청 서명 보안

항상 SigV4 구현을 검증하세요.

  1. 모든 요청에 HTTPS 사용
  2. 서명에 필요한 모든 헤더 포함
  3. 타임스탬프가 AWS 서버 시간과 5분 이내인지 확인
  4. AWS 자격 증명을 정기적으로 교체
  5. 가능하면 장기 자격 증명 대신 IAM 역할 사용
// 요청 타임스탬프 검증
const validateTimestamp = (amzDate) => {
  const now = new Date();
  const requestTime = new Date(amzDate);
  const diff = Math.abs(now - requestTime);

  // AWS는 5분 이상 된 요청을 거부합니다
  if (diff > 5 * 60 * 1000) {
    throw new Error('요청 타임스탬프가 너무 오래되었습니다. 서버 시계를 동기화하세요.');
  }
};

Apidog를 사용한 SP-API 통합 테스트

SP-API 통합을 테스트해야 하는 이유

아마존 SP-API 통합은 복잡한 인증 흐름, 여러 엔드포인트 및 엄격한 호출 속도 제한을 포함합니다. 테스트는 다음을 돕습니다.

SP-API 테스트를 위한 Apidog 설정

1단계: SP-API OpenAPI 사양 가져오기

Apidog는 OpenAPI 3.0 사양을 지원합니다. 아마존의 SP-API 사양을 가져오세요.

  1. 아마존 리포지토리에서 SP-API OpenAPI 사양 다운로드
  2. Apidog에서 새 프로젝트 생성
  3. 사양 파일 가져오기
  4. 자격 증명을 위한 환경 변수 구성

2단계: 환경 변수 구성

환경별 변수를 설정합니다.

기본 URL (샌드박스): https://sandbox.sellingpartnerapi-na.amazon.com
기본 URL (프로덕션): https://sellingpartnerapi-na.amazon.com
LWA 액세스 토큰: {{lwa_access_token}}
AWS 액세스 키: {{aws_access_key}}
AWS 시크릿 키: {{aws_secret_key}}
지역: us-east-1

3단계: 사전 요청 스크립트 생성

Apidog의 사전 요청 스크립트로 SigV4 서명을 자동화하세요.

// SigV4 서명을 위한 Apidog 사전 요청 스크립트
const crypto = require('crypto');

const accessKey = apidog.variables.get('aws_access_key');
const secretKey = apidog.variables.get('aws_secret_key');
const accessToken = apidog.variables.get('lwa_access_token');
const region = apidog.variables.get('region');

const method = apidog.request.method;
const url = new URL(apidog.request.url);
const body = apidog.request.body;

// SigV4 서명 생성
const signer = new SigV4Signer(accessKey, secretKey, region);
const signedHeaders = signer.sign(method, url.href, body, {
  'x-amz-access-token': accessToken
});

// 요청을 위한 헤더 설정
apidog.request.headers = {
  ...apidog.request.headers,
  ...signedHeaders.headers
};

4단계: 테스트 시나리오 생성

일반적인 워크플로를 위한 테스트 시나리오를 구축하세요.

// 테스트: 지난 24시간 동안의 주문 가져오기
// 1. OAuth 코드를 토큰으로 교환
// 2. GetOrders 엔드포인트 호출
// 3. 응답 구조 검증
// 4. 주문 ID 추출
// 5. 각 주문에 대해 GetOrderItems 호출

const ordersResponse = await apidog.send({
  method: 'GET',
  url: '/orders/v0/orders',
  params: {
    createdAfter: new Date(Date.now() - 86400000).toISOString(),
    marketplaceIds: 'ATVPDKIKX0DER'
  }
});

apidog.assert(ordersResponse.status === 200, '주문 요청 실패');
apidog.assert(ordersResponse.data.payload.orders.length > 0, '주문을 찾을 수 없음');

// 후속 요청을 위해 주문 ID 저장
const orderIds = ordersResponse.data.payload.orders.map(o => o.amazon_order_id);
apidog.variables.set('order_ids', JSON.stringify(orderIds));

SP-API 응답 모의

Apidog의 스마트 모의 기능을 사용하여 개발 중 SP-API 응답을 시뮬레이션하세요.

// GetOrders에 대한 모의 응답
{
  "payload": {
    "orders": [
      {
        "amazon_order_id": "112-{{randomNumber}}-{{randomNumber}}",
        "order_status": "Unshipped",
        "purchase_date": "{{now}}",
        "order_total": {
          "currency_code": "USD",
          "amount": "{{randomFloat 10 500}}"
        }
      }
    ],
    "next_token": null
  }
}

Apidog에서 API 설계-첫 번째 연습

API를 더 쉽게 구축하고 사용하는 방법을 발견하세요