Panoramica
Le API storefront espongono al tuo sito tutto quello che serve per vendere online, leggendo i dati direttamente da Vemlora. Non devi duplicare prodotti, prezzi o giacenze: aggiorni il gestionale e il sito si adegua.
Catalogo e ricerca
Prodotti, varianti, filtri a faccette, menu e vetrina delle collezioni.
Spedizioni e coupon
Metodi di spedizione e codici sconto gestiti dal gestionale.
Checkout sicuro
Prenotazione della merce e totali calcolati lato server.
Ordini nel gestionale
Ogni pagamento riuscito diventa un ordine pronto da evadere.
Il browser parla solo con il tuo sito. È il server del tuo sito a chiamare Vemlora, con una chiave segreta che il cliente non vede mai.
Come ottenere l'accesso
Le API sono disponibili su richiesta. Attiviamo l'accesso per la tua azienda, colleghiamo il catalogo e ti consegniamo la chiave segreta da usare nelle chiamate.
- Scrivici dalla pagina contatti indicando il nome della tua azienda su Vemlora e l'indirizzo del sito.
- Ricevi la chiave e salvala in una variabile d'ambiente del server, ad esempio VEMLORA_STOREFRONT_KEY.
- Configura il gestionale: metodi di spedizione, codici sconto e catalogo che vuoi pubblicare.
Guida rapida
Tutte le chiamate partono dallo stesso indirizzo base e portano la chiave nell'header x-storefront-key.
https://app.vemlora.it/api/storefront1. Prova con curl
curl "https://app.vemlora.it/api/storefront/catalog/products?search=tazza&perPage=12" \
-H "x-storefront-key: $VEMLORA_STOREFRONT_KEY"2. Crea un piccolo client sul server
Un'unica funzione aggiunge chiave e intestazioni a ogni chiamata. L'esempio è in TypeScript, ma la logica è la stessa in qualunque linguaggio.
// server/utils/vemlora.ts — gira SOLO sul server del tuo sito
const BASE_URL = "https://app.vemlora.it/api/storefront";
export async function vemlora<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(BASE_URL + path, {
...init,
headers: {
"content-type": "application/json",
"x-storefront-key": process.env.VEMLORA_STOREFRONT_KEY!,
...init.headers,
},
});
const body = await response.json().catch(() => null);
if (!response.ok && response.status !== 409) {
throw new Error(body?.statusMessage ?? "Errore Vemlora " + response.status);
}
return body as T;
}
// Esempio: prima pagina del catalogo, prodotti disponibili
const catalog = await vemlora("/catalog/products?available=true&sort=newest");3. Esponi i dati al tuo frontend
Il frontend chiama una rotta del tuo sito, che a sua volta chiama Vemlora. Così la chiave non lascia mai il server.
// server/api/shop/products.get.ts (Nuxt / Nitro)
export default defineEventHandler(async (event) => {
const query = new URLSearchParams(getQuery(event) as Record<string, string>);
return vemlora("/catalog/products?" + query.toString());
});Autenticazione
Ogni richiesta deve includere la chiave segreta nell'header x-storefront-key. Senza chiave, o con una chiave sbagliata, la risposta è 401.
La chiave è come una password
Chi la possiede può creare prenotazioni e ordini a nome del tuo negozio. Usala solo lato server, non salvarla nel repository e scrivici subito se pensi che sia stata esposta: la rigeneriamo.
Convenzioni
- Formato. Richieste e risposte sono JSON in UTF-8. Per le POST invia l'header content-type application/json.
- Importi. Tutti i prezzi sono interi in centesimi di euro: 1490 significa 14,90 €. Nessun arrotondamento a carico tuo.
- Date. Le date sono stringhe ISO 8601 in UTC.
- Paginazione. Le liste paginate restituiscono page, perPage, total e totalPages.
- Errori. Un errore ha uno statusCode HTTP e un statusMessage in italiano, già adatto da mostrare all'utente.
Catalogo
Prodotti, varianti, prezzi e disponibilità pubblicati da Vemlora. I dati si aggiornano da soli a ogni sincronizzazione del magazzino: il tuo sito li legge e basta. Nei campi, game è il primo livello del catalogo (reparto o linea) ed expansion è la collezione; rarity, collectorNumber, condition, language e foil sono attributi del modulo Trading Card e restano vuoti per gli altri prodotti.
/catalog/productsCerca nel catalogo
Elenco paginato dei prodotti con i conteggi delle faccette calcolati sugli stessi filtri: la barra filtri del tuo sito mostra sempre numeri coerenti.
Parametri query
| Nome | Tipo | Descrizione |
|---|---|---|
search | string | Testo libero su nome, collezione e codice collezione (e numero, con il modulo Trading Card). |
game | string | Slug del primo livello del catalogo (reparto o linea), ad esempio casa-cucina. |
category | string | Slug della categoria. |
expansion | string | Slug di una collezione, o più slug separati da virgola. |
tag | string | Tag del prodotto, ad esempio SALDI. |
language | string | Una o più lingue separate da virgola, ad esempio it,en. Attributo del modulo Trading Card. |
condition | string | Una o più condizioni separate da virgola. Attributo del modulo Trading Card. |
rarity | string | Una o più rarità separate da virgola. Attributo del modulo Trading Card. |
available | boolean | true per i soli disponibili, false per i soli esauriti. Omesso: tutti. |
hasImage | boolean | true per escludere i prodotti senza immagine. |
hasHdImage | boolean | true per i soli prodotti con immagine ad alta risoluzione. |
singlesOnly | boolean | Filtro del modulo Trading Card: true per i soli prodotti con una condizione (le carte singole), esclusi i sigillati. |
minPriceCents | integer | Prezzo minimo in centesimi. |
maxPriceCents | integer | Prezzo massimo in centesimi. |
sort | string | relevance (predefinito), price_asc, price_desc, name_asc, name_desc, newest. |
page | integer | Pagina, da 1. Predefinita 1. |
perPage | integer | Risultati per pagina, da 1 a 60. Predefinito 24. |
{
"items": [ /* oggetti prodotto, vedi sotto */ ],
"page": 1,
"perPage": 24,
"total": 318,
"totalPages": 14,
"facets": {
"languages": [],
"conditions": [],
"rarities": [],
"games": [{ "value": "casa-cucina", "label": "Casa & Cucina", "count": 318 }],
"categories": [{ "value": "tazze-mug", "label": "Tazze e mug", "count": 42 }],
"expansions": [{ "value": "autunno-2026", "label": "Autunno 2026", "count": 37 }],
"price": { "min": 390, "max": 45900 }
}
}/catalog/products/{slug}Dettaglio prodotto
Un singolo prodotto con tutte le varianti acquistabili. Accetta lo slug oppure il blueprintId numerico.
- Ogni variante combina lingua, condizione e finitura, e il variantId le riporta in quest'ordine dopo il blueprintId: senza il modulo Trading Card valgono na, na e reg. Il carrello del tuo sito deve lavorare sul variantId, non sullo slug.
- Risponde 404 se il prodotto non esiste.
{
"blueprintId": 274512,
"slug": "tazza-gres-350-salvia",
"name": "Tazza in gres 350 ml",
"nameIt": "Tazza in gres 350 ml",
"description": "…",
"game": "Casa & Cucina",
"gameSlug": "casa-cucina",
"category": "Tazze e mug",
"categorySlug": "tazze-mug",
"expansion": "Autunno 2026",
"expansionIt": "Collezione Autunno 2026",
"expansionCode": "aut26",
"expansionSlug": "autunno-2026",
"rarity": "",
"collectorNumber": "",
"images": ["https://…/small.jpg"],
"imagesLarge": ["https://…/large.jpg"],
"imageWidth": 734,
"hasHdImage": true,
"tags": ["SALDI"],
"languages": [],
"conditions": [],
"minPriceCents": 1490,
"totalAvailableQty": 7,
"available": true,
"variants": [
{
"variantId": "274512-na-na-reg",
"language": "",
"condition": "",
"foil": false,
"priceCents": 1490,
"quantity": 7
}
]
}/catalog/expansionsCollezioni in vetrina
Collezioni (expansion) ordinate dalla più recente, con fino a quattro immagini di copertina e il prezzo di partenza. Ideale per i banner della home.
Parametri query
| Nome | Tipo | Descrizione |
|---|---|---|
limit | integer | Numero massimo di collezioni restituite. |
minProducts | integer | Esclude le collezioni con meno prodotti disponibili. Se nessuna supera la soglia, le restituisce tutte. |
{
"items": [
{
"slug": "autunno-2026",
"name": "Collezione Autunno 2026",
"code": "aut26",
"expansionId": 3472,
"game": "Casa & Cucina",
"gameSlug": "casa-cucina",
"categorySlug": "tazze-mug",
"coverImage": "https://…",
"coverImages": ["https://…", "https://…"],
"coverProduct": "Tazza in gres 350 ml",
"products": 37,
"fromPriceCents": 390
}
],
"updatedAt": "2026-10-18T07:30:00.000Z"
}/catalog/recommendedProdotti consigliati
Suggerimenti a partire dal carrello o dai prodotti visti: prima la stessa collezione, poi lo stesso reparto, infine il resto del catalogo. La sezione non resta mai vuota.
Corpo JSON
| Nome | Tipo | Descrizione |
|---|---|---|
seedSlugs | string[] | Slug di riferimento, massimo 20. |
excludeSlugs | string[] | Slug da escludere, ad esempio quelli già nel carrello. Massimo 50. |
limit | integer | Da 1 a 20. Predefinito 5. |
{
"seedSlugs": ["tazza-gres-350-salvia"],
"excludeSlugs": [],
"limit": 6
}{ "items": [ /* oggetti prodotto disponibili e con immagine */ ] }Spedizioni e coupon
Metodi di spedizione e codici sconto si configurano nel gestionale. Il sito li legge da qui, così una modifica in Vemlora vale subito anche online.
/shipping-methodsMetodi di spedizione
I metodi attivi, ordinati dal più economico.
- I metodi con international = false valgono solo per l'Italia, quelli con international = true per gli altri paesi dell'Unione Europea.
- Il checkout rifiuta un metodo non valido per il paese di consegna: filtra l'elenco prima di mostrarlo.
{
"items": [
{
"id": "6650f0c2a1b2c3d4e5f60718",
"name": "Poste Italiane - Raccomandata",
"priceCents": 590,
"maxWeight": null,
"maxValueCents": null,
"international": false
}
]
}/coupons/validateVerifica un codice sconto
Controlla un coupon creato in Ordini & Bolle → Codici sconto e calcola lo sconto sul totale merce. Serve per mostrarlo nel carrello: il checkout lo ricontrolla comunque.
Corpo JSON
| Nome | Tipo | Descrizione |
|---|---|---|
code obbligatorio | string | Il codice inserito dal cliente. Maiuscole e minuscole sono equivalenti. |
cartTotalCents obbligatorio | integer | Totale merce in centesimi, spedizione esclusa. |
- Lo sconto non supera mai il totale merce: un coupon fisso più alto del carrello lo azzera, senza generare credito.
- Con la spedizione gratuita restano comunque da pagare 50 centesimi, il minimo addebitabile da Stripe.
- Possibili motivi di rifiuto: Codice mancante, Codice non valido, Codice scaduto, Codice esaurito, Ordine minimo non raggiunto.
{ "code": "estate10", "cartTotalCents": 5000 }// Coupon valido
{ "valid": true, "code": "ESTATE10", "discountCents": 500, "freeShipping": false }
// Coupon rifiutato
{ "valid": false, "reason": "Codice scaduto" }Checkout
Il checkout blocca la merce per 15 minuti mentre il cliente paga. Prezzi, disponibilità, spedizione e sconto li calcola sempre Vemlora: quello che invia il browser non conta.
/checkout/reservePrenota la merce
Valida i dati del cliente, verifica le disponibilità, applica spedizione e coupon e prenota le quantità. Restituisce i totali da far pagare.
Corpo JSON
| Nome | Tipo | Descrizione |
|---|---|---|
lines obbligatorio | array | Righe del carrello: { variantId, quantity }. |
shippingMethodId obbligatorio | string | Id di un metodo di spedizione valido per il paese. |
couponCode | string | Codice sconto facoltativo. |
customer obbligatorio | object | name, surname, email e phone. Il telefono accetta da 6 a 15 cifre con + facoltativo. |
address obbligatorio | object | street, city, zip, province e country. Per l'Italia CAP a 5 cifre e provincia di 2 lettere; all'estero niente provincia. |
invoice | object | Facoltativo. kind private richiede taxCode; kind company richiede companyName e vatNumber, con sdiCode o pec facoltativi. |
- Paesi accettati: i 27 stati dell'Unione Europea, indicati con il codice ISO a due lettere.
- Per un'azienda italiana senza SDI né PEC la fattura va nel cassetto fiscale con codice 0000000.
- Un 409 non è un errore del tuo codice: la merce è finita o il coupon non vale più. Mostra il motivo al cliente e fagli aggiornare il carrello.
{
"lines": [{ "variantId": "274512-na-na-reg", "quantity": 1 }],
"shippingMethodId": "6650f0c2a1b2c3d4e5f60718",
"couponCode": "ESTATE10",
"customer": { "name": "Giulia", "surname": "Rossi", "email": "giulia@example.com", "phone": "+393331234567" },
"address": { "street": "Via Roma 1", "city": "Milano", "zip": "20121", "province": "MI", "country": "IT" },
"invoice": { "kind": "private", "taxCode": "RSSGLI90A41F205X" }
}// 200: merce prenotata
{
"ok": true,
"reservationId": "0f8e2a6c-4d1b-4c55-9a1e-2b7f3c9d8e10",
"expiresAt": "2026-10-18T10:15:00.000Z",
"lines": [{ "variantId": "274512-na-na-reg", "name": "Tazza in gres 350 ml", "quantity": 1, "unitPriceCents": 1490, "…": "…" }],
"shippingMethod": { "id": "6650f0c2…", "name": "Poste Italiane - Raccomandata", "priceCents": 590 },
"coupon": { "code": "ESTATE10", "discountCents": 149, "freeShipping": false },
"couponError": null,
"totals": { "itemsTotalCents": 1490, "discountCents": 149, "shippingCents": 590, "totalCents": 1931 }
}
// 409: disponibilità cambiata o coupon non più valido
{
"ok": false,
"issues": [{ "variantId": "274512-na-na-reg", "name": "Tazza in gres 350 ml", "reason": "insufficient_stock", "requested": 2, "available": 1 }],
"couponError": "Codice esaurito"
}/checkout/releaseRilascia una prenotazione
Rimette subito in vendita la merce di una prenotazione non pagata. Chiamalo quando il cliente cambia carrello, spedizione o dati dopo aver aperto il pagamento.
Corpo JSON
| Nome | Tipo | Descrizione |
|---|---|---|
reservationId obbligatorio | string | L'id restituito da /checkout/reserve. |
- Annulla prima il PaymentIntent su Stripe: la merce di un pagamento ancora valido non deve tornare in vendita.
- Le prenotazioni dimenticate scadono da sole dopo 15 minuti.
{ "reservationId": "0f8e2a6c-4d1b-4c55-9a1e-2b7f3c9d8e10" }{ "ok": true, "released": true }Ordini
Quando il pagamento va a buon fine, il sito chiede a Vemlora di creare l'ordine. L'ordine compare subito nel gestionale, pronto per DDT, fattura e spedizione.
/ordersCrea l'ordine
Crea l'ordine dalla prenotazione salvata al checkout. Righe, prezzi e dati del cliente vengono dalla prenotazione, non dalla richiesta: un browser manomesso non può cambiarli.
Corpo JSON
| Nome | Tipo | Descrizione |
|---|---|---|
paymentIntentId obbligatorio | string | Id del PaymentIntent Stripe pagato. |
reservationId obbligatorio | string | La prenotazione salvata nei metadati del pagamento. |
amountReceivedCents | integer | Importo incassato secondo Stripe. Se diverso dal totale, l'ordine viene segnalato all'operatore. |
claimConfirmationEmail | boolean | true se vuoi occuparti tu dell'email di conferma. Vedi sendConfirmationEmail nella risposta. |
- È idempotente sul paymentIntentId: pagina di ritorno e webhook possono chiamarlo entrambi, l'ordine resta uno. La seconda chiamata riceve existing = true.
- Una prenotazione appartiene a un solo pagamento: un PaymentIntent diverso riceve 409.
- Se la prenotazione era già scaduta l'ordine viene creato lo stesso, perché il cliente ha pagato, e stockConflict = true lo segnala in gestionale.
- orderNumber segue la numerazione configurata in Gestione contatori per i documenti degli ordini web.
{
"paymentIntentId": "pi_3Q…",
"reservationId": "0f8e2a6c-4d1b-4c55-9a1e-2b7f3c9d8e10",
"amountReceivedCents": 1931,
"claimConfirmationEmail": true
}{
"ok": true,
"existing": false,
"orderId": "6712ab…",
"orderNumber": "2026/0123",
"stockConflict": false,
"sendConfirmationEmail": true,
"order": {
"createdAt": "2026-10-18T10:00:00.000Z",
"customer": { "…": "…" },
"address": { "…": "…" },
"invoice": null,
"shippingMethod": { "…": "…" },
"lines": [ "…" ],
"coupon": { "code": "ESTATE10", "discountCents": 149, "freeShipping": false },
"totals": { "itemsTotalCents": 1490, "discountCents": 149, "shippingCents": 590, "totalCents": 1931 }
}
}Flusso di acquisto completo
Ecco come si incastrano le chiamate, dalla vetrina all'ordine in gestionale. Il pagamento passa dal tuo account Stripe.
- 1
Mostra il catalogo
Menu, vetrina e ricerca leggono catalogo e collezioni. Puoi metterli in cache per qualche minuto.
- 2
Componi il carrello
Salva variantId e quantità. Per i suggerimenti usa i consigliati, per lo sconto la verifica coupon.
- 3
Prenota la merce
Al momento del pagamento chiama /checkout/reserve: blocca la merce per 15 minuti e restituisce il totale.
- 4
Incassa con Stripe
Crea il PaymentIntent sul tuo account Stripe con totals.totalCents e reservationId nei metadati.
- 5
Crea l'ordine
A pagamento riuscito chiama /orders dalla pagina di ritorno e dal webhook. L'ordine arriva in Vemlora.
Il checkout in codice
// 1. Blocca la merce e ottieni i totali calcolati da Vemlora
const reservation = await vemlora("/checkout/reserve", {
method: "POST",
body: JSON.stringify({ lines, shippingMethodId, couponCode, customer, address, invoice }),
});
if (!reservation.ok) return showIssues(reservation.issues, reservation.couponError);
// 2. Crea il pagamento sul TUO account Stripe con il totale restituito
const paymentIntent = await stripe.paymentIntents.create({
amount: reservation.totals.totalCents,
currency: "eur",
metadata: { reservationId: reservation.reservationId },
});
// 3. A pagamento riuscito (pagina di ritorno E webhook "payment_intent.succeeded")
const result = await vemlora("/orders", {
method: "POST",
body: JSON.stringify({
paymentIntentId: paymentIntent.id,
reservationId: paymentIntent.metadata.reservationId,
amountReceivedCents: paymentIntent.amount_received,
claimConfirmationEmail: true,
}),
});
// 4. Solo una delle due chiamate riceve sendConfirmationEmail = true
if (result.sendConfirmationEmail) await sendOrderEmail(result.order);Se il cliente cambia idea
Se il cliente modifica carrello, spedizione o indirizzo dopo aver aperto il pagamento, annulla il PaymentIntent e rilascia la prenotazione. Poi ripeti la prenotazione con i nuovi dati.
// Il cliente torna al carrello dopo aver aperto il pagamento:
await stripe.paymentIntents.cancel(paymentIntentId); // prima Stripe…
await vemlora("/checkout/release", { // …poi Vemlora
method: "POST",
body: JSON.stringify({ reservationId }),
});Errori
Gli errori usano i normali codici HTTP. Il corpo contiene sempre un messaggio leggibile.
{
"statusCode": 400,
"statusMessage": "Campo non valido: CAP"
}| Codice | Significato | Cosa fare |
|---|---|---|
400 | Richiesta non valida | Un campo manca o non rispetta il formato. statusMessage indica quale. |
401 | Chiave non valida | Controlla l'header x-storefront-key e che la chiave non sia stata rigenerata. |
404 | Risorsa non trovata | Prodotto inesistente, oppure API non ancora attivate per la tua azienda. |
409 | Conflitto | Merce esaurita, coupon non più valido o prenotazione già usata. Mostra il motivo al cliente. |
503 | Catalogo non pronto | Il catalogo non è ancora stato pubblicato. Riprova dopo la prima sincronizzazione. |
Buone pratiche
La chiave resta sul server
Chiama le API solo dal backend del tuo sito e conserva la chiave in una variabile d'ambiente. Mai nel codice del browser o in un'app mobile.
Metti in cache il catalogo
Menu e collezioni cambiano solo alla sincronizzazione: una cache di qualche minuto rende il sito più veloce. Non mettere in cache checkout e ordini.
Conferma l'ordine due volte
Chiama /orders sia dalla pagina di ritorno sia dal webhook Stripe. Grazie all'idempotenza non avrai doppioni, e nessun pagamento resterà senza ordine.
Fidati dei totali di Vemlora
Fai pagare sempre totals.totalCents restituito dal checkout, mai un importo calcolato nel browser.
Domande frequenti
Serve un sito fatto in un linguaggio particolare?
No. Le API parlano JSON su HTTPS: funzionano con Nuxt, Next.js, Laravel, WordPress con un plugin custom o qualunque backend capace di fare una richiesta HTTP.
Chi incassa i pagamenti?
Tu, con il tuo account Stripe. Vemlora calcola i totali, blocca la merce e registra l'ordine, ma non tocca mai i soldi.
Gli ordini del sito finiscono nel gestionale?
Sì, appena il pagamento è confermato. Li trovi in Ordini & Bolle con tutti i dati per DDT, fattura e spedizione, e il magazzino si aggiorna da solo.
Posso usare i coupon che creo in Vemlora?
Sì. I codici creati in Ordini & Bolle → Codici sconto valgono subito sul sito, con limiti di utilizzo, ordine minimo e scadenza.
Cosa succede se un prodotto si esaurisce durante il checkout?
La prenotazione risponde 409 con l'elenco delle righe non disponibili e le quantità rimaste. Il cliente aggiorna il carrello e riprova, senza pagare nulla.
Pronto a collegare il tuo sito?
Raccontaci il tuo progetto: attiviamo le API per la tua azienda e ti aiutiamo nei primi passi.