Amazon Selling Partner API in produzione: quello che la documentazione non ti dice
Ho costruito da zero l'integrazione con Amazon SP API per un sistema di fulfillment multi-brand. Ecco le sfide reali: autenticazione a tre livelli, rate limiting per account, feed polling in Lambda, e webhook idempotenti.
Amazon Selling Partner API è la sostituzione di MWS — più moderna, più consistente, meglio documentata. In teoria.
In pratica, ho trascorso settimane a capire perché le mie richieste tornassero 403, perché il rate limiter si comportasse diversamente per ogni endpoint, e come fare polling su un feed senza far andare in timeout la Lambda.
Questo articolo racconta quello che ho imparato costruendo un’integrazione SP API in produzione per un sistema di fulfillment che gestisce ordini su più marketplace EU e più seller account Amazon.
Il contesto
L’app gestisce tre flussi principali:
- Ordini: Amazon notifica via webhook → recuperiamo i dettagli completi → normalizziamo e pubblichiamo su EventBridge per il sistema di fulfillment
- Inventario: eventi di cambio stock → aggiornamento bulk su tutti i marketplace EU via Feeds API
- Tracking: confermiamo la spedizione ad Amazon dopo che il warehouse ha processato l’ordine
Ogni flusso tocca SP API in modo diverso, e ognuno ha le sue particolarità.
Autenticazione: tre livelli, tre token, tre scadenze
Questo è il punto dove la maggior parte delle guide ti abbandonano dopo il primo paragrafo.
SP API usa tre meccanismi di autenticazione in sequenza:
1. LWA (Login with Amazon) — OAuth 2.0 per ottenere l’access_token. Devi fare refresh ogni ora circa, e il token cambia in base al grant_type (refresh_token per operazioni sugli ordini, client_credentials con scope specifici per le notifiche).
2. AWS STS AssumeRole — SP API richiede di assumere un IAM role specifico (SP_API_ROLE_ARN) per ottenere credenziali AWS temporanee. Scadono ogni ora.
3. SigV4 — Ogni richiesta HTTP va firmata con le credenziali STS usando la firma AWS standard.
Il problema non è capire i singoli passi — è gestire tre token con tre scadenze diverse in modo robusto.
private async ensureAccessToken(grantType: OAuthGrantType, scope?: OAuthScope): Promise<void> {
const now = Date.now();
const needsRefresh =
!this.accessToken ||
!this.accessTokenExpiry ||
now > this.accessTokenExpiry ||
this.accessTokenGrantType !== grantType ||
(this.accessTokenScope || null) !== (scope || null);
if (needsRefresh) {
try {
await this.generateAccessToken(grantType, scope);
} catch {
// retry once — LWA può rispondere lento in burst
await this.generateAccessToken(grantType, scope);
}
}
}
private async ensureRoleCredentials(): Promise<void> {
const now = Date.now();
const bufferMs = 5 * 60 * 1000; // refresh 5 minuti prima della scadenza
if (!this.roleCredentials || now + bufferMs > this.roleCredentialsExpiry) {
await this.generateRoleCredentials();
}
}
Tre cose non ovvie qui:
- Il buffer di 5 minuti sulle credenziali STS serve per evitare race condition: se la credenziale scade mentre una richiesta è in volo, ottieni un 403 difficile da diagnosticare.
- L’
access_tokendeve essere cachato separatamente pergrant_typee scope. Usare un tokenclient_credentialsdove serverefresh_tokenrestituisce 403 senza spiegazioni utili. - Un singolo retry sul fetch del token è sufficiente — LWA ha burst di lentezza, non outage prolungati.
Rate limiting: non è optional
Amazon pubblica i rate limit per ogni endpoint nella documentazione. Quello che non dice chiaramente è che i limiti si applicano per seller account, non per applicazione.
Se gestisci più seller account dallo stesso Lambda, devi isolare il rate limiter per account altrimenti i tentativi di un account consumano la quota degli altri.
Ho usato un token bucket per ogni endpoint:
private createRateLimiter(): RateLimiter {
return new RateLimiter([
// Feeds API — i più critici, limiti molto bassi
{ apiName: SPApiEndpoint.CREATE_FEED, refillRate: 0.0083, maxTokens: 15, sleepTime: 1000 },
{ apiName: SPApiEndpoint.CREATE_FEED_DOCUMENT, refillRate: 0.5, maxTokens: 15 },
{ apiName: SPApiEndpoint.GET_FEED, refillRate: 0.0222, maxTokens: 10 },
{ apiName: SPApiEndpoint.GET_FEED_DOCUMENT, refillRate: 0.0222, maxTokens: 10 },
]);
}
refillRate: 0.0083 significa circa 1 richiesta ogni 2 minuti. CREATE_FEED è l’endpoint più restrittivo — Amazon permette pochissime creazioni di feed per ora. Se lo superi ottieni un 429 che blocca l’aggiornamento inventario per tutti i marketplace.
Ogni seller account crea la propria istanza del rate limiter. Il Lambda casha le istanze in memoria tra un’invocazione e l’altra (warm Lambda), quindi il limiter non riparte da zero ad ogni messaggio SQS.
Feeds API: come funziona il polling in Lambda
Per aggiornare l’inventario su più marketplace in bulk, SP API usa le Feeds API. Il flow è:
- Crea un
feedDocument— ottieni un URL pre-firmato per l’upload - Carica il contenuto in formato JSON Lines (un oggetto per riga) su quell’URL
- Crea il feed con il
feedDocumentId— ottieni unfeedId - Polla
GET /feeds/{feedId}fino a quandoprocessingStatusdiventaDONEoFATAL - Scarica il report (GZIP), parsalo per trovare errori per SKU
Il punto critico è il passo 4: Amazon non ti notifica quando il feed è pronto. Devi pollare.
private async waitForFeedProcessingAndVerify(feedId: string): Promise<BulkUpdateError[]> {
const maxAttempts = 24;
const pollInterval = 10_000; // 10 secondi
let attempts = 0;
while (attempts < maxAttempts) {
const feed = await this.amazonClient.getFeed(feedId);
if (feed.processingStatus === FeedProcessingStatus.DONE) {
if (feed.resultFeedDocumentId) {
return this.verifyFeedReport(feed.resultFeedDocumentId);
}
return [];
}
if (feed.processingStatus === FeedProcessingStatus.FATAL) {
throw new Error(`Feed FATAL: ${feedId}`);
}
await sleep(pollInterval);
attempts++;
}
throw new Error(`Feed timeout dopo 240s: ${feedId}`);
}
24 tentativi × 10 secondi = 4 minuti di attesa massima, dentro il timeout della Lambda (10 minuti). Nella pratica, la maggior parte dei feed viene processata in 30-60 secondi. Il caso peggiore l’ho visto quando Amazon aveva problemi interni — il feed andava in FATAL dopo il timeout.
Un’alternativa sarebbe Step Functions con waitForTaskToken — avvii il feed, passi il token ad Amazon come callback URL (non supportato da SP API), e l’esecuzione si sospende. SP API non supporta questo pattern, quindi polling è l’unica via.
Il report finale è un file GZIP con una riga JSON per ogni SKU processato. Parsarlo per estrarre gli errori richiede gunzipSync — niente di complesso, ma non documentato nell’esempio ufficiale.
Webhook: idempotenza obbligatoria
Per ricevere notifiche degli ordini, devi registrare una “destination” (il tuo SQS ARN) e poi sottoscriverti al notification type ORDER_CHANGE.
Il problema è che createDestination e createSubscription non sono idempotenti. Se richiami il setup (ad esempio a ogni deploy), Amazon crea duplicati e ricevi le notifiche più volte.
async subscribeToWebhooks(queueArn: string): Promise<void> {
const destinations = await this.getDestinations();
let destinationId = destinations
.find(d => d.resource.sqs.arn === queueArn)
?.destinationId;
if (!destinationId) {
destinationId = await this.createDestination(queueArn);
}
const existing = await this.getSubscription(AmazonNotificationType.ORDER_CHANGE);
if (!existing) {
await this.createSubscription(destinationId, AmazonNotificationType.ORDER_CHANGE);
}
}
Prima controlli se la destination esiste già (confrontando l’ARN), poi controlli se la sottoscrizione esiste. Solo se mancano le crei. Questa Lambda gira ogni ora via EventBridge — se SP API perde la sottoscrizione (è successo), la ricrea automaticamente entro un’ora.
Una nota sulla migration: fino a poco tempo fa SP API usava ORDER_STATUS_CHANGE come notification type. Amazon l’ha deprecato in favore di ORDER_CHANGE, che include più informazioni e un payload strutturato diversamente. La migration ha richiesto di aggiornare il parsing del payload oltre al tipo sottoscritto.
Multi-account: un Lambda, più seller
Il sistema gestisce brand multipli, ognuno con il proprio seller account Amazon e le proprie credenziali SP API. Un singolo Lambda update-inventory processa messaggi SQS per tutti i brand.
La soluzione è caching lazy in memoria:
const amazonServices = new Map<AmazonSellerAccount, AmazonInventoryService>();
for (const account of AMAZON_SELLER_ACCOUNTS) {
let service = amazonServices.get(account);
if (!service) {
const credentials = await secretsService.getCredentials(account);
service = new AmazonInventoryService(credentials, createRateLimiter(), logger);
amazonServices.set(account, service);
}
const { errors } = await service.updateInventory(products);
// errori raccolti come SQS batchItemFailures
}
Ogni account ha il proprio service (con le proprie credenziali cachate e il proprio rate limiter). I messaggi falliti per un account tornano in coda senza bloccare gli altri — SQS batch item failure granulare, non failure dell’intero batch.
Quello che rifarei diversamente
La cache degli SKU esterni non ha TTL. Alcuni SKU sono gestiti da un servizio esterno e devono essere esclusi prima del fulfillment. La lista viene cachata a livello di modulo, senza TTL: persiste finché la Lambda è warm. Se la lista cambia, rimane stale fino al prossimo recycle del container. Aggiungerei un TTL di 15 minuti.
Il polling del feed non usa backoff. 10 secondi fissi per 24 tentativi. Per feed grandi (molti SKU), i primi tentativi sono quasi certamente prematuri. Un backoff esponenziale con cap a 30 secondi ridurrebbe le chiamate GET /feeds inutili.
Nessun circuit breaker su SP API. Se Amazon è down, i messaggi si accumulano in SQS fino al DLQ dopo N retry. Un circuit breaker che smette di processare quando il failure rate supera una soglia ridurrebbe il rumore e il consumo di credenziali di autenticazione.
Amazon SP API è lavorabile in produzione, ma richiede di costruire infrastruttura attorno ad essa — il client ufficiale non gestisce token rotation, rate limiting, o polling. Se stai iniziando, considera questi problemi fin dall’architettura, non come afterthought.