Saleor è una piattaforma e-commerce open source in Python con API GraphQL. Qui vedi come funzionano catalogo e checkout, che cosa devi sapere sui webhook (limiti di tempo e firma) e dove la guida che trovi in giro rischia di essere superata.
Saleor in breve: un'unica API GraphQL, e un checkout al posto del carrello
Saleor è scritto in Python e Django e si usa quasi interamente attraverso un'API GraphQL: catalogo, ordini, clienti, spedizioni e configurazione passano da lì. Se arrivi da Medusa, la differenza più evidente non è il linguaggio ma lo stile: una sola porta d'ingresso in cui descrivi i campi che ti servono, invece di molte rotte REST (per il contratto REST e i principi API-first vedi l'articolo sull'architettura MACH). Se invece arrivi da Shopify, c'è un'altra differenza di modello da conoscere: secondo la documentazione sul checkout, Saleor non ha un oggetto «carrello» separato. Il checkout è il carrello: nasce con le prime righe, accumula indirizzi e metodo di consegna, e alla fine si trasforma in ordine.
Una conseguenza di sicurezza che la stessa pagina mette in chiaro: un checkout è identificato da un ID basato su UUID, e chiunque conosca quell'ID può leggerlo e modificarlo. I dati del carrello, l'email e gli indirizzi sono quindi leggibili da chi ha l'ID; invece i dati dell'utente, le transazioni e i metadati privati richiedono proprietario o permessi di staff o di app. Tratta l'ID di un checkout come un segreto di sessione: non metterlo in un indirizzo condiviso, non registrarlo nei log di terze parti.
Il catalogo: una query, i canali, e un filtro che sta cambiando
La lettura del catalogo usa lo schema Relay delle connessioni: edges con node, e pageInfo per ripartire dal cursore, come nel ciclo di paginazione visto nell'articolo introduttivo.
Il parametro channel merita attenzione: in Saleor un canale raggruppa valuta, prodotti disponibili, metodi di spedizione e di pagamento, e i prezzi cambiano da canale a canale. Per questo l'ordinamento per prezzo, nello schema, dichiara di richiedere un canale per funzionare: senza, il prezzo non è definito.
query Catalogo($channel: String!, $first: Int!, $after: String) {
products(channel: $channel, first: $first, after: $after, sortBy: { field: PRICE, direction: ASC }) {
totalCount
pageInfo { hasNextPage endCursor }
edges {
node {
id
name
slug
thumbnail(size: 512) { url alt }
pricing { onSale priceRange { start { gross { amount currency } } } }
category { name slug }
}
}
}
}
Ho controllato questa query contro lo schema pubblicato nel repository di Saleor (ramo main, scaricato il 2026-11-12) con un piccolo script che percorre ogni campo e verifica che esista sul tipo giusto: 23 campi controllati, nessuno mancante.
Non è una validazione completa (gli argomenti e i tipi delle variabili non sono controllati) e la query non è stata inviata a un'istanza Saleor. Un dettaglio che lo schema mostra e che le guide meno recenti non dicono: l'argomento filter di products è segnato come deprecato in favore di where.
Per questo la query qui sopra non filtra e ordina soltanto: se ti serve filtrare, parti da where.
Il checkout in quattro mutazioni
Il flusso minimo ha quattro passi, e l'ordine non è arbitrario: l'indirizzo viene prima del metodo di consegna perché i metodi disponibili dipendono da dove spedisci, e il metodo viene prima del completamento perché cambia il totale.
Le mutazioni sono checkoutCreate (con le righe e il canale), checkoutShippingAddressUpdate, checkoutDeliveryMethodUpdate e checkoutComplete.
Ognuna restituisce un campo errors con field, code e message: gli errori di dominio (indirizzo non valido, prodotto esaurito) arrivano lì, non come errori HTTP, e se non li leggi il flusso sembra riuscito.
# 1. Il checkout nasce con le righe: in Saleor non esiste un oggetto "carrello" a parte
mutation CheckoutCreate($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout { id totalPrice { gross { amount currency } } lines { id quantity } }
errors { field code message }
}
}
# 2. L'indirizzo di spedizione: dopo, il checkout elenca i metodi di consegna possibili
mutation ShippingAddress($id: ID!, $address: AddressInput!) {
checkoutShippingAddressUpdate(id: $id, shippingAddress: $address) {
checkout { id shippingMethods { id name price { amount currency } } }
errors { field code message }
}
}
# 3. La scelta del metodo di consegna ricalcola il totale
mutation DeliveryMethod($id: ID!, $deliveryMethodId: ID!) {
checkoutDeliveryMethodUpdate(id: $id, deliveryMethodId: $deliveryMethodId) {
checkout { id totalPrice { gross { amount currency } } }
errors { field code message }
}
}
# 4. Il completamento trasforma il checkout in ordine
mutation Complete($id: ID!) {
checkoutComplete(id: $id) {
order { id number status }
confirmationNeeded
confirmationData
errors { field code message }
}
}
Anche queste quattro operazioni sono state verificate contro lo schema (49 campi, nessuno mancante), non eseguite contro un negozio. Il confronto con lo schema ha un effetto concreto rispetto a molte guide: oggi le mutazioni identificano il checkout con l'argomento id,
mentre checkoutId e token sono marcati come deprecati. Un tutorial che usa ancora checkoutId funziona finché il campo non viene rimosso, e poi smette di colpo.
Sul completamento la documentazione elenca le condizioni: indirizzi validi, scorte disponibili e pagamenti elaborati (a meno che il canale ammetta ordini non pagati o il totale sia zero). Come costruire il pagamento con Stripe lo trovi nell'articolo sui pagamenti headless. I checkout non completati vengono cancellati dopo un tempo che dipende dal contenuto:
sei ore se senza righe, trenta giorni se anonimi con righe, novanta se legati a un utente o a un'email.
Webhook: due modi di ricevere, un limite di tempo, una firma da verificare
I webhook sono il modo in cui Saleor avvisa un altro sistema di ciò che accade (un ordine creato, un pagamento ricevuto) o chiede aiuto durante un'operazione (calcolare le tasse, elencare i metodi di spedizione). La documentazione distingue due categorie: gli eventi asincroni, inviati quando la richiesta che li ha causati è terminata; gli eventi sincroni, inviati subito durante la richiesta GraphQL, per cui il tempo di risposta del tuo endpoint si somma a quello della richiesta del cliente. Se il tuo servizio impiega quattro secondi, il cliente aspetta quattro secondi in più al passo del checkout. Quanto pesino i tempi sulle metriche che il cliente percepisce lo vedi nell'articolo su Core Web Vitals e SEO.
Il limite scritto nella documentazione consultata il 2026-11-12 è di 20 secondi per l'intero scambio, di cui al massimo 2 per la connessione e 18 per la risposta, e la stessa pagina avverte che più chiamate sincrone nella stessa operazione possono sommarsi e far scadere l'API di Saleor. Tradotto in regola di progetto: un endpoint che risponde ai webhook sincroni deve essere veloce e separato da quelli pesanti, e per gli eventi asincroni conviene mettere i messaggi in una coda e lavorarli con calma.
La firma serve a sapere che la richiesta viene davvero da Saleor. L'intestazione Saleor-Signature, secondo la pagina sulla firma, contiene una firma JWS con algoritmo RS256 e payload staccato se non hai impostato una chiave segreta sul webhook;
la firma con HMAC SHA-256 è indicata come deprecata. Con RS256 verifichi con una chiave pubblica che Saleor pubblica in un JWKS, e non devi custodire nessun segreto condiviso. Il listato seguente simula entrambi i lati: genera una coppia di chiavi, firma un corpo come farebbe Saleor,
e lo verifica come dovresti farlo tu, costruendo il testo da verificare con l'intestazione e il corpo grezzo ricodificato in base64url.
import { createPublicKey, generateKeyPairSync, sign, verify } from 'node:crypto';
const b64url = (b: Buffer | string) => Buffer.from(b).toString('base64url');
// --- Lato Saleor (simulato): firma JWS RS256 con payload "staccato" ---
const { privateKey, publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
function firma(corpo: string): string {
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const firmaBytes = sign('RSA-SHA256', Buffer.from(`${header}.${b64url(corpo)}`), privateKey);
return `${header}..${b64url(firmaBytes)}`; // il payload al centro e' vuoto: e' staccato
}
// --- Lato tuo: verifica con la chiave pubblica (in produzione arriva dal JWKS) ---
const jwk = publicKey.export({ format: 'jwk' });
function verifica(jws: string, corpoGrezzo: string): boolean {
const [header, , firmaB64] = jws.split('.');
return verify(
'RSA-SHA256',
Buffer.from(`${header}.${b64url(corpoGrezzo)}`),
createPublicKey({ key: jwk, format: 'jwk' }),
Buffer.from(firmaB64, 'base64url'),
);
}
const corpo = JSON.stringify({ event: 'ORDER_CREATED', data: { order: { number: 42 } } });
const jws = firma(corpo);
console.log('corpo intatto :', verifica(jws, corpo));
console.log('corpo alterato :', verifica(jws, corpo.replace('42', '43')));
console.log('corpo riscritto:', verifica(jws, JSON.stringify(JSON.parse(corpo), null, 2)));
// Output:
// corpo intatto : true
// corpo alterato : false
// corpo riscritto: false
Ho eseguito il file con Node 24 e l'uscita è quella mostrata. Il terzo caso è quello che fa perdere ore: il corpo «riscritto» ha lo stesso contenuto JSON ma spazi diversi, e la verifica fallisce. La documentazione lo dice in chiaro (devi fornire la stringa grezza del corpo, non un oggetto già interpretato),
ed è per questo che un framework che legge e rinterpreta il corpo prima di arrivare al tuo codice rompe la verifica. Una limitazione del listato: la chiave pubblica qui viene dalla stessa coppia generata in locale; in produzione la prendi dall'indirizzo JWKS dell'istanza Saleor, e la libreria jose citata dalla documentazione fa il recupero e la verifica al posto tuo.
Quando Saleor, e quando no
Saleor ha senso se il tuo team preferisce Python o GraphQL, se ti serve un modello con più canali di vendita già nel nucleo (negozi per paese, B2B e B2C sulla stessa installazione), e se sei disposto a gestire l'infrastruttura: database, code per i task asincroni, aggiornamenti. Non ha senso se vuoi un servizio gestito senza sistemista (per quello vedi l'articolo su Shopify) o se tutto il tuo codice è TypeScript e non vuoi un secondo linguaggio ( Medusa ). Il costo reale, in entrambi i casi open source, non è la licenza: è il tempo di chi tiene il sistema in piedi.
Che cosa sai fare adesso
Sai spiegare perché in Saleor il checkout fa da carrello e che cosa implica per la sicurezza dell'ID, leggere il catalogo per canale con paginazione, guidare il checkout in quattro mutazioni controllando errors,
e ricevere un webhook rispettando il limite di venti secondi e verificando la firma sul corpo grezzo. Nel prossimo articolo costruiamo lo
storefront con Next.js che consuma una di queste API.
Fonti
- Saleor: Checkout overview - assenza del modello carrello, ID UUID e permessi di accesso; consultata il 2026-11-12
- Saleor: Checkout lifecycle - requisiti di completamento e tempi di scadenza dei checkout; consultata il 2026-11-12
- Saleor: Webhooks overview - eventi sincroni e asincroni, limite di 20 secondi; consultata il 2026-11-12
- Saleor: Payload Signature - intestazione Saleor-Signature, JWS RS256 con payload staccato, HMAC deprecato; consultata il 2026-11-12
- Saleor: schema.graphql (repository del progetto, ramo main) - esistenza dei campi, argomenti deprecati (
filter,checkoutId); scaricato il 2026-11-12

Commenti
Caricamento commenti...
Accedi per commentare