Salta al contenuto principale

05 - Saleor: E-commerce GraphQL-Native, Checkout API Avanzata e 160+ Webhook

Saleor è un backend e-commerce in Python con API GraphQL. Vedi il checkout in quattro mutazioni, i limiti di tempo dei webhook e come verificarne la firma.

Cosa ne pensi di questo articolo?

05 - Saleor: E-commerce GraphQL-Native, Checkout API Avanzata e 160+ Webhook
HeadlessCommerceFoto:panumas nikhomkhaiPexels

In Breve

Saleor è un backend e-commerce in Python con API GraphQL. Vedi il checkout in quattro mutazioni, i limiti di tempo dei webhook e come verificarne la firma.

Headless Commerce 9 min

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 graphql commerce checkout API webhook headless ecommerce python

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

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti