DEV Community

Cover image for Intégration API Amazon SP : Tutoriel Pas à Pas
Antoine Laurent
Antoine Laurent

Posted on • Originally published at apidog.com

Intégration API Amazon SP : Tutoriel Pas à Pas

En bref

L'API Amazon Selling Partner (SP-API) est une API REST qui permet un accès programmatique aux données du vendeur pour les commandes, l'inventaire, les fiches produits et l'exécution des commandes. Elle utilise l'authentification OAuth 2.0 avec des rôles IAM, nécessite la signature AWS SigV4 et applique des limites de débit qui varient selon les points de terminaison (de 0,1 à 100 requêtes par seconde). Ce guide couvre la configuration du compte, l'authentification, les points de terminaison principaux, les abonnements aux webhooks et les stratégies de déploiement en production.

Essayez Apidog dès aujourd'hui

💡 Astuce pratique : Apidog simplifie les tests d'intégration d'API. Testez vos points de terminaison SP-API, validez les flux OAuth, inspectez les signatures de requêtes et déboguez les problèmes d'authentification dans un seul espace de travail. Importez des spécifications d'API, simulez des réponses et partagez des scénarios de test avec votre équipe.

Qu'est-ce que l'API Amazon SP ?

L'API Amazon Selling Partner (SP-API) est une API REST qui fournit un accès programmatique aux données centrales du vendeur. Elle remplace le Marketplace Web Service (MWS) avec une sécurité, des performances et des fonctionnalités accrues.

Fonctionnalités Clés

SP-API gère :

  • Récupération des commandes et mises à jour de statut
  • Gestion des stocks sur les places de marché
  • Création, mise à jour et suppression de fiches produits
  • Gestion des expéditions FBA (Expédié par Amazon)
  • Tarification des produits et analyse concurrentielle
  • Génération de rapports et d'analyses
  • Gestion du contenu A+
  • Analyses de marque et données publicitaires

Comparaison SP-API vs MWS

Caractéristique SP-API MWS (Hérité)
Architecture RESTful JSON Basée sur XML
Authentification OAuth 2.0 + IAM Jeton d'authentification MWS
Sécurité Signature AWS SigV4 Jetons simples
Limites de débit Dynamique par point de terminaison Quotas fixes
Places de marché Points de terminaison unifiés Spécifique à la région
Statut Actuel Obsolète (Déc 2025)

Migrez toutes les intégrations MWS vers SP-API dès maintenant. Amazon arrête MWS en décembre 2025.

Vue d'ensemble de l'architecture API

Amazon utilise une structure API régionale :

https://sellingpartnerapi-na.amazon.com (Amérique du Nord)
https://sellingpartnerapi-eu.amazon.com (Europe)
https://sellingpartnerapi-fe.amazon.com (Extrême-Orient)
Enter fullscreen mode Exit fullscreen mode

Toutes les requêtes nécessitent :

  1. Signature AWS SigV4
  2. Jeton d'accès OAuth
  3. Rôle IAM autorisé
  4. ID de requête pour le traçage

Places de marché prises en charge

Région Places de marché Point de terminaison API
Amérique du Nord US, CA, MX sellingpartnerapi-na.amazon.com
Europe UK, DE, FR, IT, ES, NL, SE, PL, TR, EG, IN, AE, SA sellingpartnerapi-eu.amazon.com
Extrême-Orient JP, AU, SG, BR sellingpartnerapi-fe.amazon.com

Démarrage : Configuration du Compte et d'IAM

Étape 1 : Créer un Compte Développeur Amazon

  1. Rendez-vous sur le Centre Développeur Amazon
  2. Connectez-vous avec un compte ayant accès à Seller Central
  3. Accédez à l'API Selling Partner depuis le dashboard
  4. Acceptez l'Accord Développeur

Étape 2 : Enregistrer Votre Application

  1. Connectez-vous à Seller Central
  2. Menu Applications et Services > Développer des applications
  3. Cliquez sur Ajouter une nouvelle application
  4. Remplissez :
    • Nom de l'application
    • Type d'application : Auto-développée ou Tierce partie
    • Cas d'utilisation
    • URI de redirection (HTTPS obligatoire pour OAuth)

Après soumission, récupérez :

  • ID d'application
  • ID client
  • Secret client

Stockez ces identifiants dans un .env :

# .env
AMAZON_APPLICATION_ID="amzn1.application.xxxxx"
AMAZON_CLIENT_ID="amzn1.account.xxxxx"
AMAZON_CLIENT_SECRET="votre_secret_client_ici"
AMAZON_SELLER_ID="votre_id_vendeur_ici"
AWS_ACCESS_KEY_ID="votre_cle_d_acces_aws"
AWS_SECRET_ACCESS_KEY="votre_cle_secrete_aws"
AWS_REGION="us-east-1"
Enter fullscreen mode Exit fullscreen mode

Étape 3 : Créer un Rôle IAM pour SP-API

  1. Connectez-vous à la Console AWS IAM
  2. Rôles > Créer un rôle
  3. Sélectionnez Un autre compte AWS
  4. ID du compte Amazon selon la région :
    • Amérique du Nord : 906394416454
    • Europe : 336853085554
    • Extrême-Orient : 774466381866

Étape 4 : Attacher la Politique IAM

Ajoutez cette politique :

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

Nommez le rôle clairement, ex : SellingPartnerApiRole. Notez l'ARN.

Étape 5 : Lier le Rôle IAM à l'Application

  1. Retour à Seller Central > Développer des applications
  2. Sélectionnez votre application
  3. Modifier > ARN du rôle IAM
  4. Ajoutez l'ARN du rôle créé et sauvegardez

Statut « Lié » attendu après quelques minutes.

Flux d'Authentification OAuth 2.0

Comprendre l'OAuth SP-API

Flux standard :

1. L'utilisateur clique "Autoriser" dans votre application
2. Redirection vers Amazon OAuth
3. Connexion & acceptation des permissions
4. Redirection avec code d'autorisation
5. Échange du code contre un jeton LWA
6. Échange du jeton LWA contre un jeton d'accès SP-API
7. Utilisation du jeton d'accès pour les appels API (signés SigV4)
8. Rafraîchissement du jeton toutes les heures
Enter fullscreen mode Exit fullscreen mode

Étape 6 : Générer l'URL d'Autorisation OAuth

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, // Protection CSRF
    scope: 'sellingpartnerapi::notifications'
  });

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

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

console.log(`Rediriger l'utilisateur vers : ${authUrl}`);
Enter fullscreen mode Exit fullscreen mode

Portées OAuth Requises

Portée Description Cas d'utilisation
sellingpartnerapi::notifications Recevoir des notifications Abonnements aux webhooks
sellingpartnerapi::migration Migrer depuis MWS Intégrations héritées

La majorité des droits d'accès sont contrôlés côté IAM, pas OAuth.

Étape 7 : Échanger le Code contre un Jeton LWA

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, // 1h
    token_type: data.token_type
  };
};

// Route de rappel OAuth
app.get('/callback', async (req, res) => {
  const { spapi_oauth_code, state } = req.query;

  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('Échange de jeton échoué :', error);
    res.status(500).send('Authentification échouée');
  }
});
Enter fullscreen mode Exit fullscreen mode

Étape 8 : Échanger le Jeton LWA contre des Identifiants SP-API

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();

  // Échange via 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;
};
Enter fullscreen mode Exit fullscreen mode

Étape 9 : Implémenter l'Actualisation du Jeton

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
  };
};

// Middleware pour assurer un jeton valide
const ensureValidToken = async (sellerId) => {
  const seller = await db.sellers.findById(sellerId);

  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;
};
Enter fullscreen mode Exit fullscreen mode

Signature des requêtes AWS SigV4

Comprendre SigV4

Toutes les requêtes SP-API doivent être signées en AWS Signature Version 4.

Processus de signature SigV4 (exemple Node.js)

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();

    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');

    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');

    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');

    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);
  }
}

// Utilisation
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
});
Enter fullscreen mode Exit fullscreen mode

Utiliser le SDK AWS pour SigV4

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();
};
Enter fullscreen mode Exit fullscreen mode

API Commandes

Récupérer des Commandes

const getOrders = async (accessToken, options = {}) => {
  const params = new URLSearchParams({
    createdAfter: options.createdAfter,
    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);
};

// Exemple
const orders = await getOrders(accessToken, {
  createdAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
  orderStatuses: ['Unshipped', 'PartiallyShipped'],
  marketplaceIds: ['ATVPDKIKX0DER']
});
Enter fullscreen mode Exit fullscreen mode

Structure de la Réponse de Commande

{
  "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",
        "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=="
  }
}
Enter fullscreen mode Exit fullscreen mode

Obtenir les Articles de Commande

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

// Utilisation
const orderItems = await getOrderItems(accessToken, '112-1234567-1234567');
Enter fullscreen mode Exit fullscreen mode

Réponse attendue :

{
  "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."
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Mettre à Jour le Statut d'Expédition

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,
      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);
};

// Exemple
await confirmShipment(accessToken, '112-1234567-1234567', {
  carrierCode: 'USPS',
  trackingNumber: '9400111899223456789012',
  items: [
    { orderItemId: '12345678901234', quantity: 2 }
  ]
});
Enter fullscreen mode Exit fullscreen mode

Codes de Transporteur Courants

Transporteur Code Transporteur
USPS USPS
FedEx FEDEX
UPS UPS
DHL DHL
Postes Canada CANADA_POST
Royal Mail ROYAL_MAIL
Poste australienne AUSTRALIA_POST
Amazon Logistics AMZN_UK

API Inventaire

Obtenir les Résumés d'Inventaire

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);
};

// Exemple
const inventory = await getInventorySummaries(accessToken, {
  granularityId: 'ATVPDKIKX0DER',
  sellerSkus: ['MYSKU-001', 'MYSKU-002']
});
Enter fullscreen mode Exit fullscreen mode

Structure de la Réponse d'Inventaire

{
  "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"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Mettre à Jour l'Inventaire

SP-API ne propose pas de point de terminaison direct pour modifier le stock. Gérez l'inventaire via :

  1. Expéditions FBA (création de plans d'expédition)
  2. Commandes MFN (décrémentation automatique)
  3. Mises à jour des fiches produits

Pour 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);
};
Enter fullscreen mode Exit fullscreen mode

API Fiches Produits

Obtenir les Fiches Produits

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);
};

// Exemple
const listings = await getListings(accessToken, {
  identifiers: ['B08N5WRWNW', 'B09JQKJXYZ'],
  itemTypes: ['ASIN']
});
Enter fullscreen mode Exit fullscreen mode

Structure de la Réponse de Fiche Produit

{
  "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
}
Enter fullscreen mode Exit fullscreen mode

Créer ou Mettre à Jour des Fiches Produits

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);
};
Enter fullscreen mode Exit fullscreen mode

Supprimer une Fiche Produit

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);
};
Enter fullscreen mode Exit fullscreen mode

API Rapports

Créer des Planifications de Rapports

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);
};

// Types de rapports fréquents
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'
};

// Exemple
const report = await createReport(accessToken, REPORT_TYPES.ORDERS, {
  marketplaceIds: ['ATVPDKIKX0DER'],
  startTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
  endTime: new Date()
});
Enter fullscreen mode Exit fullscreen mode

Obtenir le Document de Rapport

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();

  if (documentInfo.payload.compressionAlgorithm === 'GZIP') {
    const decompressed = await decompressGzip(content);
    return decompressed;
  }

  return content;
};
Enter fullscreen mode Exit fullscreen mode

API Notifications

Créer des Abonnements

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

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

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

// Types d'événements
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'
};

// Exemple
await createSubscription(accessToken, {
  destinationArn: 'arn:aws:sns:us-east-1:123456789012:sp-api-notifications',
  name: 'OrderStatusNotifications',
  eventCode: EVENT_CODES.ORDER_STATUS_CHANGE,
  marketplaceIds: ['ATVPDKIKX0DER']
});
Enter fullscreen mode Exit fullscreen mode

Configurer la Destination 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 });
};

// Politique SNS
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'
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

Traiter les Notifications

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;

  // Vérification de la signature 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Limitation du Débit et Quotas

Comprendre les Limites de Débit

Catégorie de Point de Terminaison Limite de Débit Limite de Rafale
Commandes 10 requêtes/seconde 20
Articles de Commande 5 requêtes/seconde 10
Inventaire 2 requêtes/seconde 5
Fiches Produits 10 requêtes/seconde 20
Rapports 0.5 requêtes/seconde 1
Notifications 1 requête/seconde 2
FBA Entrant 2 requêtes/seconde 5

Surveillez l'en-tête x-amzn-RateLimit-Limit pour vos quotas.

Implémenter la Gestion de la Limitation du Débit

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);

      // Contrôle du quota
      const rateLimit = response.headers.get('x-amzn-RateLimit-Limit');
      const retryAfter = response.headers.get('Retry-After');

      if (retryAfter) {
        console.warn(`Limite de débit atteinte. Réessayez après : ${retryAfter} secondes`);
      }

      return response;
    } catch (error) {
      if (error.message.includes('429') && attempt < maxRetries) {
        const retryAfter = error.headers?.get('Retry-After') || Math.pow(2, attempt);
        console.log(`Limite de débit atteinte. Réessai dans ${retryAfter}s...`);
        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(`Service indisponible. Réessai dans ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
      } else {
        throw error;
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Implémentation de la File d'Attente de Requêtes

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;
  }
}

// Exemple : file d'attente pour les commandes (10 req/s)
const ordersQueue = new RateLimitedQueue(10, 20);
const orders = await ordersQueue.add(() => getOrders(accessToken, options));
Enter fullscreen mode Exit fullscreen mode

Bonnes Pratiques de Sécurité

Gestion des Identifiants

Ne stockez jamais vos credentials dans le code :

// Mauvaise pratique (NE PAS FAIRE)
const AWS_ACCESS_KEY = 'AKIAIOSFODNN7EXAMPLE';
const AWS_SECRET = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY';

// Bonne pratique
const AWS_ACCESS_KEY = process.env.AWS_ACCESS_KEY_ID;
const AWS_SECRET = process.env.AWS_SECRET_ACCESS_KEY;

// Idéal : Secrets Manager
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);
};
Enter fullscreen mode Exit fullscreen mode

Exigences de Stockage des Jetons

Respectez :

  1. Chiffrement au repos (ex : AES-256)
  2. Chiffrement en transit (HTTPS/TLS 1.2+)
  3. Contrôles d'accès (limité à des comptes de service)
  4. Journalisation d'audit (tous les accès/journaux de rafraîchissement)
  5. Rotation (actualisez avant expiration)
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;
  }
}
Enter fullscreen mode Exit fullscreen mode

IAM Moindre Privilège

N'autorisez que ce qui est strictement nécessaire :

{
  "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/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Limitez au maximum l'usage de * en production.

Sécurité de la Signature de Requête

Validez votre implémentation SigV4 :

  1. Toujours utiliser HTTPS
  2. Inclure tous les en-têtes requis dans la signature
  3. L'horodatage ne doit pas dépasser 5 min de différence avec AWS
  4. Faites pivoter vos identifiants AWS fréquemment
  5. Privilégiez les rôles IAM temporaires
const validateTimestamp = (amzDate) => {
  const now = new Date();
  const requestTime = new Date(amzDate);
  const diff = Math.abs(now - requestTime);

  if (diff > 5 * 60 * 1000) {
    throw new Error('Horodatage de la requête trop ancien. Synchronisez l\'horloge de votre serveur.');
  }
};
Enter fullscreen mode Exit fullscreen mode

Continuez à tester chaque point de terminaison et flux OAuth avec Apidog pour accélérer le développement, valider vos signatures et automatiser la documentation d'API.

Intégrez ces étapes dans vos outils CI/CD pour garantir une intégration SP-API robuste, sécurisée et scalable.

Top comments (0)