Integration API v1
API push inbound: il vostro sistema invia catalogo e prezzi a PriceTag via HTTP. PriceTag non scarica dati dal gestionale e non scrive verso di esso in v1.
Pensata per team tecnici di ERP, POS, middleware e system integrator. Contratto machine-readable: OpenAPI 3 · Versione 1.0 · Aggiornata 2026-09-23.
Cosa fa (e cosa no)
Sezione intitolata “Cosa fa (e cosa no)”PriceTag è il layer di presentazione a scaffale (ESL + app negozio). Il gestionale / POS resta, di norma, la source of truth commerciale; PriceTag tiene lo snapshot operativo per mostrare e pubblicare i prezzi.
Fa
- Upsert catalogo e aggiornamenti prezzo per EAN
- Auth con token di integrazione legato a un negozio
- Esito parziale per batch (
accepted/rejected[]) - Staging prezzi in review in-app (default)
Non fa (v1)
- Sync bidirezionale o webhook verso l’ERP
- Esporre lo snapshot completo o le ops interne
- Connettori vendor-specific (SAP, Danea, Softcare, …)
- Auto-publish hardware / Base Station nello stesso request
Un solo contratto HTTP: chi spinge (ERP, middleware, script, Google Apps Script) è irrilevante.
Prerequisiti
Sezione intitolata “Prerequisiti”- Account PriceTag con accesso al negozio target.
storeId— UUID del negozio (in app o fornito in onboarding).- Token di integrazione — in app: Impostazioni → Connessioni → Create Token.
- Il plaintext compare solo alla creazione o rotazione: salvatelo subito in un secret store.
- La revoca invalida subito il token (kill switch).
- Capacità di fare
HTTPSPOST/GETcon headerAuthorizatione body JSON.
Base URL
Sezione intitolata “Base URL”| Ambiente | Base URL |
|---|---|
| Production | https://webapp-pricetag-production.up.railway.app/api |
Prefisso risorse:
{BASE}/v1/integration/stores/{storeId}Esempio:
https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/19c75782-90ed-4821-85d7-50018ec0beb9/healthNei POST usate Content-Type: application/json.
Autenticazione
Sezione intitolata “Autenticazione”Usate uno di questi header:
Authorization: Bearer <integration_token>X-PriceTag-Key: <integration_token>| Situazione | Risposta |
|---|---|
| Scope | Solo lo storeId legato al token |
| JWT utente app | Non accettato |
| Token di un altro store | 403 |
| Token assente, invalido o revocato | 401 |
Il flag UI “enabled” sul token non blocca le chiamate: il controllo operativo è la revoca.
Concetti chiave
Sezione intitolata “Concetti chiave”Chiave prodotto = EAN
Sezione intitolata “Chiave prodotto = EAN”Tutti i prodotti partner sono indirizzati per EAN (stringa, trim). Gli ID interni PriceTag non fanno parte del contratto.
Campi JSON in inglese
Sezione intitolata “Campi JSON in inglese”| Campo | Significato |
|---|---|
ean |
Codice a barre / EAN |
name |
Nome prodotto (catalog) |
price |
Prezzo |
department |
Reparto (opzionale, catalog) |
sku |
Codice interno / SKU (opzionale) |
brand non è richiesto (legacy accettato se inviato). Non inviare nome / prezzo (chiavi italiane): il body viene rifiutato.
Formato prezzo
Sezione intitolata “Formato prezzo”| Direzione | Formato |
|---|---|
Ingresso (POST) |
Decimale con punto, stringa o number — es. "3.49" o 3.49 |
Uscita (GET prodotto) |
Display italiano — es. "3,49 €" |
PriceTag applica le stesse regole di normalizzazione dell’import prezzi in-app.
Successo parziale
Sezione intitolata “Successo parziale”I POST batch rispondono di solito HTTP 200 anche se alcune righe falliscono. Controllate sempre:
accepted/created/updatedrejected[]con{ ean, code, message }
Body non valido → 400. Operazioni non applicabili allo snapshot → 422.
Idempotenza
Sezione intitolata “Idempotenza”Header opzionale:
Idempotency-Key: <stringa ≤ 128 caratteri>| Caso | Comportamento |
|---|---|
| Stessa chiave + stesso body | Stessa risposta memorizzata (TTL 24 h); nessuna doppia applicazione |
| Stessa chiave + body diverso | 409 (idempotency_mismatch) |
| Chiave assente | Ogni request è indipendente |
Usate l’ID del job / batch del vostro middleware.
Flusso prezzi e review
Sezione intitolata “Flusso prezzi e review”Di default i cambi prezzo non aggiornano subito lo scaffale: entrano in review nell’app. L’operatore deve Conferma o Scarta (il modal non si chiude con click fuori / Escape).
- Il partner invia un
POSTdi catalogo o prezzi. - L’API risponde
200constatus: "pending_review"ereviewId(se c’è un diff di prezzo). - In app: modal Nuovi prezzi → Conferma o Scarta.
- Dopo Conferma: Invia alla coda per pubblicare sulle ESL (se assegnate).
Policy di review
Sezione intitolata “Policy di review”Usata su POST .../prices.
| Valore | Effetto |
|---|---|
require_review |
Default. Staging pending; nessun cambio shelf fino a Conferma. Risposta: status: "pending_review", reviewId. |
apply_immediate |
Opt-out: last-write sullo snapshot senza modal. |
Catalog e prezzi
Sezione intitolata “Catalog e prezzi”Su POST .../catalog:
| Situazione | Effetto |
|---|---|
| EAN nuovo | Upsert con price applicato subito |
EAN esistente, price uguale allo shelf |
Aggiorna anagrafica (name, sku, department, …) |
EAN esistente, price diverso |
Anagrafica aggiornata; prezzo shelf invariato; staging review + pending_review |
Coda ESL
Sezione intitolata “Coda ESL”Rilevante su POST .../prices solo con apply_immediate (queuePolicy).
| Valore | Effetto |
|---|---|
snapshot_only |
Default. Solo snapshot (nessuna coda ESL automatica) |
enqueue_if_esl |
Encola se il prodotto ha un’etichetta ESL assegnata |
enqueue_always |
Encola sempre |
Con require_review, dopo Conferma l’operatore usa Invia alla coda in app (come una modifica manuale). Non c’è auto-publish hardware nel request di integrazione.
Endpoints
Sezione intitolata “Endpoints”Path assoluto = Base URL + path sotto.
| Metodo | Path | Descrizione |
|---|---|---|
GET |
/v1/integration/stores/{storeId}/health |
Token valido + negozio raggiungibile |
GET |
/v1/integration/stores/{storeId}/products/{ean} |
Lettura prodotto per EAN |
POST |
/v1/integration/stores/{storeId}/catalog |
Upsert catalogo (batch) |
POST |
/v1/integration/stores/{storeId}/prices |
Aggiornamento prezzi (batch) |
Verifica credenziali e storeId.
Risposta 200:
{ "ok": true, "storeId": "19c75782-90ed-4821-85d7-50018ec0beb9", "enabled": true}enabled riflette lo stato UI; non blocca le mutazioni.
Lettura prodotto
Sezione intitolata “Lettura prodotto”Lettura puntuale (debug / riconciliazione).
Risposta 200:
{ "ean": "8051277182755", "name": "POMODORI SECCHI", "price": "3,49 €", "department": "Ortofrutta", "sku": "ART-9912"}404 se l’EAN non esiste nel negozio.
Catalog (upsert)
Sezione intitolata “Catalog (upsert)”Upsert per EAN. In v1 l’unico mode supportato è "upsert".
Request:
{ "items": [ { "ean": "8051277182755", "name": "POMODORI SECCHI", "price": "3.49", "department": "ORTOFRUTTA", "sku": "ART-9912" } ], "mode": "upsert"}| Campo | Obbligatorio | Note |
|---|---|---|
ean |
sì | Non vuoto |
name |
sì | Non vuoto dopo trim |
price |
sì | Vedi formato prezzo |
department |
no | Label reparto |
sku |
no | Mappato a codice interno |
Header consigliato: Idempotency-Key.
Risposta 200: vedi Risposta batch. Con diff di prezzo su EAN esistenti: status: "pending_review" + reviewId.
Aggiorna solo i prezzi di prodotti già presenti (per EAN).
Request:
{ "items": [ { "ean": "8051277182755", "price": "3.29" } ], "queuePolicy": "snapshot_only", "reviewPolicy": "require_review"}| Campo | Default | Note |
|---|---|---|
items[].ean |
— | Obbligatorio |
items[].price |
— | Obbligatorio |
queuePolicy |
snapshot_only |
Soprattutto con apply_immediate |
reviewPolicy |
require_review |
Staging in-app |
EAN sconosciuto → riga in rejected (product_not_found); non fallisce l’intero batch.
Risposta batch
Sezione intitolata “Risposta batch”Schema comune ai POST catalog e prices:
{ "accepted": 1, "created": 0, "updated": 1, "status": "pending_review", "reviewId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "rejected": [ { "ean": "9999999999999", "code": "product_not_found", "message": "Prodotto non trovato per EAN" } ]}| Campo | Significato |
|---|---|
accepted |
Righe accettate / mergeate |
created |
Nuovi prodotti (tipicamente catalog) |
updated |
Prodotti aggiornati |
status |
pending_review o applied |
reviewId |
UUID della review (se pending_review) |
rejected[] |
Righe non applicate, con codice macchina |
PriceTag non restituisce lo snapshot completo nella risposta.
Codici HTTP
Sezione intitolata “Codici HTTP”| HTTP | Quando |
|---|---|
200 |
Batch elaborato (anche con rejected non vuoto) |
400 |
Body non valido / schema |
401 |
Token assente o invalido |
403 |
Token di un altro store |
404 |
Store o prodotto (GET) non trovato |
409 |
Idempotency-Key riusata con body diverso |
422 |
Ops non applicabili allo stato corrente |
429 |
Rate limit superato |
Codici rejected
Sezione intitolata “Codici rejected”Codici tipici in rejected[].code:
| Code | Contesto |
|---|---|
invalid_ean |
EAN mancante / vuoto |
invalid_name |
name mancante (catalog) |
invalid_price |
price non parseabile |
product_not_found |
EAN assente nello store (/prices) |
unchanged_price |
Prezzo già uguale allo shelf (/prices + review) |
skipped |
Righe saltate dal merge catalog |
| Limite | Valore |
|---|---|
Items per POST (catalog / prices) |
max 1.000 |
| Rate limit | 60 richieste / minuto per token |
| Prodotti totali per negozio | max 5.000 |
Idempotency-Key |
max 128 caratteri; TTL risposta 24 h |
Oltre i limiti: 400 (batch troppo grande) o 429 (rate).
Esempi curl
Sezione intitolata “Esempi curl”Sostituite STORE_ID e TOKEN.
curl -sS \ -H "Authorization: Bearer $TOKEN" \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/health"Catalog (upsert)
Sezione intitolata “Catalog (upsert)”curl -sS -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: catalog-job-20260923-001" \ -d '{ "items": [ { "ean": "8051277182755", "name": "POMODORI SECCHI", "price": "3.49", "department": "ORTOFRUTTA", "sku": "ART-9912" } ], "mode": "upsert" }' \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/catalog"Prices (review default)
Sezione intitolata “Prices (review default)”curl -sS -X POST \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: prices-job-20260923-001" \ -d '{ "items": [ { "ean": "8051277182755", "price": "3.29" } ], "queuePolicy": "snapshot_only", "reviewPolicy": "require_review" }' \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/prices"Risposta tipica: "status": "pending_review", "reviewId": "...". In app compare il modal Nuovi prezzi.
Lettura prodotto
Sezione intitolata “Lettura prodotto”curl -sS \ -H "Authorization: Bearer $TOKEN" \ "https://webapp-pricetag-production.up.railway.app/api/v1/integration/stores/$STORE_ID/products/8051277182755"Checklist integrazione
Sezione intitolata “Checklist integrazione”- Ottenere
storeIde creare il token in Connessioni. - Salvare il token in un vault; non committarlo.
GET .../health→ok: true.- Push di pochi EAN di test (catalog o prices) con
Idempotency-Key. - Verificare
accepted/rejectede, se prezzi cambiati,pending_review. - In app: modal Nuovi prezzi → Conferma o Scarta.
- Dopo Conferma: Invia alla coda (se presenti ESL) per pubblicare a scaffale.
- In automazione: batch ≤ 1000, max 60 req/min, ritentare solo con la stessa
Idempotency-Keya parità di body.
Fuori scope v1
Sezione intitolata “Fuori scope v1”- CRUD etichette ESL, piano negozio, reparti come risorsa dedicata
- Publish / ACK Base Station o BLE nello stesso request
replace_alldel catalogo- Webhook outbound PriceTag → ERP
- Scrittura prezzi verso il gestionale
- API billing / multi-token con permission granulari (roadmap)
- Varianti di contratto per vendor ERP
OpenAPI e supporto
Sezione intitolata “OpenAPI e supporto”- Contratto OpenAPI:
/integration-api-v1.openapi.yaml - Onboarding / token di prova: contattare PriceTag (
storeId+ guida Connessioni) - Incidenti produzione: indicare
storeId, timestamp UTC,Idempotency-Keye body (senza token in chiaro)