Richiami Alimenti

API v1

Documentazione dell'API dei richiami alimentari

API REST in JSON con i richiami alimentari pubblicati dal Ministero della Salute: campi separati e normalizzati, link all'avviso ufficiale, revoche, liste di sorveglianza per le aziende tue clienti e webhook firmati. Indirizzo base: https://richiamialimenti.it/api/v1. Specifica OpenAPI 3.1: /api/v1/openapi.json.

Inizio rapido

1. Crea un'azienda per ogni attività tua cliente e conserva il suo id:

curl -X POST https://richiamialimenti.it/api/v1/companies \
  -H "Authorization: Bearer $RICHIAMI_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Trattoria da Mario srl", "vat_number": "IT01234567890", "external_id": "CLI-042"}'

2. Leggi i richiami indicando l'azienda nell'header X-Company-Id:

curl "https://richiamialimenti.it/api/v1/recalls?per_page=20" \
  -H "Authorization: Bearer $RICHIAMI_KEY" \
  -H "X-Company-Id: 3f0c8a2e-5b1d-4c7a-9e8f-2a6b4d1c0e93"

3. Aggiungi alla lista di sorveglianza dell'azienda i prodotti, i marchi o i fornitori da controllare, e imposta il webhook per sapere subito quando un richiamo li tocca:

curl -X POST https://richiamialimenti.it/api/v1/companies/3f0c8a2e-5b1d-4c7a-9e8f-2a6b4d1c0e93/watchlist \
  -H "Authorization: Bearer $RICHIAMI_KEY" -H "Content-Type: application/json" \
  -d '{"items": [{"brand": "Salumi Rossi", "lot": "L24A123"}, {"gtin": "8001234567897"}]}'

curl -X PUT https://richiamialimenti.it/api/v1/webhook \
  -H "Authorization: Bearer $RICHIAMI_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://tuo-server.example/webhook/richiami"}'

Autenticazione e prova

Ogni chiamata (tranne la specifica OpenAPI) porta la chiave personale nell'header Authorization: Bearer rk_live_…. La chiave te la consegniamo una volta sola e la conserviamo solo in forma cifrata (hash): se la perdi la revochiamo e te ne diamo una nuova. Tienila sul server, mai in un'app o in una pagina web.

Durante la prova gratuita di 30 giorni ogni risposta ha gli header X-Richiami-Plan: trial e X-Richiami-Trial-Ends con la data di fine. Alla scadenza l'accesso si chiude da solo con l'errore trial_expired: non ci sono addebiti né rinnovi automatici. GET /account riporta stato, fine della prova, limiti e numero di aziende.

Aziende e X-Company-Id

Le aziende sono le attività alimentari tue clienti. Crei un'azienda per ogni cliente finale reale con POST /companies: ragione sociale e partita IVA o codice fiscale sono obbligatori. Controlliamo il carattere di controllo e, in un secondo momento, la partita IVA su VIES (il risultato è in vat_check). Ogni partita IVA o codice fiscale vale per una sola azienda dello stesso cliente e non si può cambiare dopo la creazione: per un nuovo cliente finale crea una nuova azienda.

Le chiamate ai dati (/recalls, /matches) vogliono l'header X-Company-Id con l'id di una tua azienda attiva: così sappiamo per chi è ogni chiamata e i dati dei tuoi clienti restano separati. Le aziende si gestiscono con GET/PATCH/DELETE /companies/{id}; DELETE disattiva l'azienda (i dati restano) e PATCH con "active": true la riattiva. Le aziende create due volte rispondono 409 company_exists con l'id di quella esistente.

Lista di sorveglianza

Ogni azienda ha una lista di voci da tenere d'occhio: GET/POST /companies/{id}/watchlist, PATCH/DELETE /companies/{id}/watchlist/{voce}. Una voce ha uno o più criteri e corrisponde a un richiamo quando corrispondono tutti i suoi criteri: «marchio X» trova ogni richiamo del marchio, «marchio X + lotto Y» solo quel lotto. Puoi aggiungere fino a 500 voci in una chiamata ({"items": [...]}) e fino a 1.000 per azienda; label ed external_id (il tuo codice articolo) tornano negli abbinamenti.

CriterioQuando corrisponde
brandStesso marchio del nostro catalogo (anche con un nome diverso, es. «Zero+4 S.r.l.» e «ZERO+4»), oppure il nome come parole intere nel marchio dell'avviso.
producerStessa azienda del catalogo, oppure il nome senza forma societaria (srl, spa…) nel produttore dell'avviso.
productTutte le parole indicate nel nome del prodotto; singolare e plurale valgono uguale («mozzarelle» trova «mozzarella»). Indica solo le parole che identificano il prodotto, non il formato.
gtinUno dei codici a barre del richiamo (8, 12, 13 o 14 cifre, con cifra di controllo corretta).
lotUno dei codici di lotto dell'avviso, ignorando spazi e punteggiatura («L 24/A-123» = «L24A123»). Almeno 3 lettere o cifre.
supplierIl nome nel distributore, nel produttore o nella catena del marchio: utile per i grossisti e le catene da cui compra l'attività.

La risposta di POST elenca per ogni voce, in matches, gli id dei richiami già in archivio che la toccano: utile per controllare subito i prodotti che l'attività ha in magazzino. Questi abbinamenti storici non mandano webhook.

Richiami e sincronizzazione

GET /recalls restituisce solo richiami attivi, al massimo 200 per pagina (per_page, default 50). Senza updated_since sono ordinati dal più recente; con updated_since sono in ordine di modifica, dal meno recente. Filtri: brand (slug o testo), producer, product, lot, gtin (completo), hazard (slug del motivo), chain (slug della catena), revoked, from e to (data del richiamo). GET /recalls/{id} restituisce un richiamo.

Paginazione

La paginazione è a cursore: se meta.next_cursor non è null, chiedi la pagina successiva con cursor=<valore> e gli stessi filtri. Il cursore resta valido anche se nel frattempo arrivano nuovi richiami.

Tenere una copia aggiornata

La prima volta scarica tutto l'archivio pagina per pagina. Poi, ogni ora o quando ricevi un webhook, chiedi GET /recalls?updated_since=<ultimo meta.last_update> e aggiorna i record per id; togli quelli elencati da GET /recalls/removed?since=…. In ogni risposta meta.last_update è l'ultima modifica di un record del feed e meta.last_check l'ultimo controllo degli avvisi del Ministero (circa ogni 2 ore): mostrali ai tuoi utenti come «dati aggiornati al».

{
  "data": [ { "id": 1950, "product": "…", "updated_at": "2026-10-23T10:12:40+02:00", "…": "…" } ],
  "meta": {
    "per_page": 50,
    "next_cursor": "WyIyMDI2LTEwLTIzIDEwOjEyOjQwIiwxOTUwXQ",
    "sort": "updated_at",
    "last_update": "2026-10-23T10:12:40+02:00",
    "last_check": "2026-10-24T08:00:03+02:00"
  }
}

Formato del record

Un richiamo ha sempre gli stessi campi; quelli che l'avviso non riporta valgono null o lista vuota. Il codice GTIN c'è solo quando l'operatore lo scrive nell'avviso o quando il codice a barre è leggibile nelle foto: il modulo del Ministero non lo prevede, quindi manca nella maggior parte dei richiami. Nel feed non ci sono foto delle confezioni: per immagini e testo integrale vale l'avviso ufficiale.

CampoContenuto
idId stabile del richiamo.
urlPagina del richiamo su richiamialimenti.it.
recall_dateData del richiamo (AAAA-MM-GG).
updated_atUltima modifica di un campo di questo record. Non cambia per lavori interni (foto, controlli).
productNome del prodotto come nell'avviso.
brand / brand_rawMarchio normalizzato del nostro catalogo ({name, slug}) e marchio come scritto nell'avviso.
company / producer_rawProduttore normalizzato ({name, city}) e produttore come scritto nell'avviso.
private_label_ofCatena di cui il marchio è marca del distributore, se lo è.
hazardsMotivi normalizzati (slug, nome, categoria), es. listeria, allergeni.
reason / warningsMotivo e avvertenze come nell'avviso.
lot / lot_codesLotti come nell'avviso e codici di lotto estratti (lettere e cifre maiuscole, senza spazi).
expiry / expiry_datesScadenza o TMC come nell'avviso e date leggibili in ISO 8601: AAAA-MM-GG, oppure AAAA-MM se l'avviso indica solo mese e anno.
plant / format / distributorStabilimento o bollo CE, formato, distributore.
gtin / gtin_sourceCodici GTIN/EAN con cifra di controllo corretta e loro origine: avviso (scritto nel testo), foto (letto dal codice a barre nelle foto), manuale (inserito da noi).
revoked / revocationSe il richiamo è stato revocato, con data, motivo e link all'avviso di revoca.
official_noticepage_url: scheda dell'avviso sul sito del Ministero; pdf_url: PDF ufficiale.
sourceFonte, licenza (CC BY 4.0) e testo di attribuzione da mostrare.

L'ultimo richiamo pubblicato

{
    "id": 1898,
    "url": "https://richiamialimenti.it/richiami/1898-tramezzini-coop-prosciutto-cotto",
    "recall_date": "2026-12-10",
    "updated_at": "2026-10-12T18:01:59+00:00",
    "product": "TRAMEZZINI - Coop Prosciutto cotto",
    "brand": {
        "name": "Coop",
        "slug": "coop"
    },
    "brand_raw": "COOP",
    "company": {
        "name": "Riva Alimentari Uniti S.r.l.",
        "city": "Nibionno"
    },
    "producer_raw": "Riva Alimentari Uniti Srl",
    "private_label_of": "Coop",
    "hazards": [
        {
            "slug": "allergeni",
            "name": "Allergeni non dichiarati",
            "category": "allergeni"
        },
        {
            "slug": "etichettatura",
            "name": "Errori di etichettatura",
            "category": "conformita"
        }
    ],
    "reason": "Errata etichettatura del prodotto. Il prodotto \"Tramezzino COOP Gamberetti 140g\" è stato erroneamente confezionato ed etichettato come \"Tramezzino COOP Prosciutto cotto 150g\". Presenza allergene CROSTACEI.",
    "warnings": "I soggetti allergici a CROSTACEI non devono consumare il prodotto. Restituire al punto vendita.",
    "lot": "A-269-F",
    "lot_codes": [
        "A269F"
    ],
    "expiry": "2026-10-31",
    "expiry_dates": [
        "2026-10-31"
    ],
    "plant": "UE IT D1T8V",
    "format": "150g",
    "distributor": "Coop Italia Società Cooperativa",
    "gtin": [],
    "gtin_source": null,
    "revoked": false,
    "revocation": null,
    "official_notice": {
        "page_url": null,
        "pdf_url": null
    },
    "source": {
        "name": "Ministero della Salute",
        "license": "CC BY 4.0",
        "license_url": "https://creativecommons.org/licenses/by/4.0/deed.it",
        "attribution": "Fonte: Ministero della Salute, avvisi di richiamo dei prodotti alimentari. Dati elaborati da Richiami Alimenti, non affiliato al Ministero."
    }
}

Abbinamenti

GET /matches (con X-Company-Id) restituisce i richiami che toccano la lista di sorveglianza dell'azienda, dal più recente, con il motivo dell'abbinamento: per ogni voce, i criteri che corrispondono e il valore trovato nel richiamo. Filtri: since e revoked. source vale new_recall o recall_updated quando l'abbinamento è arrivato con il feed (e con il webhook match.created), watchlist quando l'abbiamo trovato in archivio al momento in cui hai aggiunto la voce.

{
  "matched_at": "2026-10-23T10:15:02+02:00",
  "source": "new_recall",
  "items": [
    { "watchlist_item_id": 812, "label": "Salame del banco", "external_id": "ART-9",
      "matched": { "brand": "Salumi Rossi", "lot": "L24A123" } }
  ],
  "recall": { "id": 1950, "…": "…" }
}

Revoche e rimozioni

Il Ministero pubblica anche le revoche dei richiami (per esempio dopo analisi di revisione favorevoli). Un richiamo revocato resta nel feed con revoked: true e revocation (data e motivo dell'avviso di revoca, link alla sua scheda); arriva il webhook recall.revoked e il record cambia updated_at. Colleghiamo una revoca al richiamo originale solo quando marchio e prodotto coincidono e lotto o scadenza lo confermano; altrimenti la verifichiamo a mano prima di pubblicarla. Filtra con revoked=false per mostrare solo i richiami ancora validi.

Raramente un richiamo viene tolto dal feed: doppioni uniti a un altro record o avvisi ritirati dal Ministero. Arriva il webhook recall.removed, il richiamo compare in GET /recalls/removed e GET /recalls/{id} risponde 410 con replaced_by, l'id del richiamo che lo sostituisce se c'è.

Webhook

Imposta un URL https pubblico sulla porta standard con PUT /webhook (events facoltativo per ricevere solo alcuni eventi). La prima risposta contiene il secret per verificare le firme: conservalo, perché non lo mostriamo più (le altre risposte hanno solo secret_hint, le ultime 4 cifre). POST /webhook/secret ne crea uno nuovo, lo restituisce e invalida subito il precedente. Gli eventi partono pochi minuti dopo che un richiamo è stato importato o modificato, così arrivano completi.

EventoQuandodata
recall.createdNuovo richiamo (o tornato nel feed).{recall}
recall.updatedÈ cambiato almeno un campo del record (es. lotto corretto, GTIN aggiunto).{recall, changed: ["lot", ...]}
recall.revokedIl Ministero ha revocato il richiamo.{recall}
recall.removedRichiamo tolto dal feed (doppione unito a un altro, avviso ritirato).{recall_id, replaced_by}
match.createdUn richiamo nuovo o aggiornato tocca la lista di sorveglianza di una tua azienda.{company, matched_at, items, recall}

Richiesta

Ogni evento è un POST JSON con gli header X-Richiami-Event (tipo di evento), X-Richiami-Delivery (id univoco della consegna, uguale a id nel corpo) e X-Richiami-Signature:

X-Richiami-Signature: t=1792834560,v1=5f2b9c…e41a

{
  "id": "0b9f6c64-2f7e-4a55-9a51-3c1c3e1f9d20",
  "event": "recall.created",
  "created_at": "2026-10-23T10:15:00+02:00",
  "api_version": "v1",
  "test": false,
  "data": { "recall": { "id": 1950, "…": "…" } }
}

Verificare la firma

v1 è l'HMAC-SHA256 in esadecimale, calcolato con il tuo secret su <t>.<corpo della richiesta così come arriva>. Confronta in tempo costante e scarta le richieste con t più vecchio di 5 minuti.

// PHP
$body = file_get_contents('php://input');
preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $_SERVER['HTTP_X_RICHIAMI_SIGNATURE'] ?? '', $m);
$valid = $m && abs(time() - (int) $m[1]) <= 300
    && hash_equals(hash_hmac('sha256', $m[1] . '.' . $body, getenv('RICHIAMI_WEBHOOK_SECRET')), $m[2]);

// Node.js (Express con express.raw({ type: 'application/json' }))
const crypto = require('crypto');
const [, t, v1] = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(req.get('X-Richiami-Signature') || '') || [];
const expected = crypto.createHmac('sha256', process.env.RICHIAMI_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest('hex');
const valid = t && Math.abs(Date.now() / 1000 - t) <= 300
  && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));

# Python
import hmac, hashlib, re, time
m = re.match(r'^t=(\d+),v1=([0-9a-f]{64})$', request.headers.get('X-Richiami-Signature', ''))
expected = hmac.new(SECRET.encode(), f"{m[1]}.".encode() + request.get_data(), hashlib.sha256).hexdigest() if m else ''
valid = bool(m) and abs(time.time() - int(m[1])) <= 300 and hmac.compare_digest(expected, m[2])

Risposte e nuovi tentativi

Rispondi con un codice 2xx entro 10 secondi, poi elabora l'evento con calma. Se l'endpoint non risponde 2xx riproviamo dopo 1 minuto, 5 minuti, 15 minuti, 1 ora, 3 ore, 6 ore e 12 ore (8 tentativi in circa un giorno); non seguiamo i redirect. Lo stesso evento può arrivare più di una volta e non sempre in ordine: usa X-Richiami-Delivery per scartare i doppioni e updated_at del record per tenere la versione più recente. GET /webhook/deliveries mostra le ultime consegne con esito, tentativi e ultimo errore. In ogni caso il feed con updated_since resta la fonte completa: se il tuo server è stato giù, ti basta rileggere da lì.

Dati di prova e dati reali

Non c'è un ambiente di prova separato: anche durante i 30 giorni gratuiti l'API restituisce i dati reali, gli stessi della produzione, e gli eventi del feed sono richiami veri ("test": false). Sono di prova solo gli eventi che chiedi tu con POST /webhook/test: hanno "test": true e un richiamo finto con id 0 e nome «PRODOTTO DI PROVA - non è un richiamo». Non mostrarli mai ai tuoi utenti. Puoi scegliere il tipo con {"event": "match.created"}; al massimo 5 eventi di prova al minuto e 100 al giorno.

Fonte ufficiale e licenza

I dati vengono dagli avvisi pubblicati dal Ministero della Salute, riutilizzabili con licenza CC BY 4.0 citando la fonte. Quando mostri i dati ai tuoi clienti indica la fonte (il testo è in source.attribution) e, quando c'è, il link all'avviso ufficiale (official_notice.page_url o pdf_url). Richiami Alimenti non è affiliato al Ministero: non presentare i dati come provenienti da noi per conto del Ministero. L'estrazione dai PDF è automatica e può sbagliare: in caso di dubbio vale sempre l'avviso ufficiale, e il servizio non sostituisce le procedure del piano di autocontrollo.

Limiti

Di base: 120 richieste al minuto per chiave e 20.000 al giorno per cliente (i tuoi limiti sono in GET /account). Ogni risposta ha gli header X-RateLimit-Limit e X-RateLimit-Remaining; oltre il limite rispondiamo 429 con Retry-After. Per tenere una copia aggiornata basta una chiamata con updated_since ogni ora, più i webhook.

Errori

Gli errori sono JSON con message (in italiano, da leggere) e code (stabile, da usare nel codice). Gli errori di validazione hanno invece errors con i messaggi per ogni campo (nelle liste: items.3.gtin).

HTTPcodeSignificato
400company_requiredManca l'header X-Company-Id.
401invalid_api_keyChiave mancante, sbagliata o revocata.
403trial_expiredIl periodo di prova è finito.
403account_suspended / account_endedAccesso sospeso o terminato.
403company_deactivatedL'azienda indicata è disattivata.
403company_limitRaggiunto il numero massimo di aziende attive.
404company_not_found / recall_not_found / watchlist_item_not_foundNon esiste, o non è tra le tue.
409company_existsHai già un'azienda con questa partita IVA o codice fiscale: l'id è in company_id.
409webhook_not_configuredImposta prima l'URL del webhook.
410recall_removedRichiamo tolto dal feed; replaced_by indica quello che lo sostituisce.
422(errors)Dati non validi: il campo errors elenca i problemi per ogni campo.
422watchlist_limitLista di sorveglianza piena.
429rate_limitedTroppe richieste: riprova dopo i secondi indicati in Retry-After.

Aggiornamenti e versioni

Controlliamo gli avvisi del Ministero circa ogni 2 ore. La v1 cambia solo aggiungendo: possono comparire nuovi campi, eventi o filtri, quindi ignora i campi che non conosci. Le modifiche incompatibili arriveranno in una v2, con un preavviso. Il servizio è fornito al meglio delle nostre possibilità: se il sito del Ministero cambia o non è raggiungibile i dati possono arrivare in ritardo, e te lo diciamo. Per domande e problemi scrivi a [email protected].

Le informazioni contenute in Richiamialimenti.it sono prese dal sito del Ministero della Salute e potrebbero non essere complete o contenere errori. Per approfondimenti si rimanda al sito ufficiale del Ministero della Salute.

Avvisi sui richiami

Notifica gratuita a ogni nuovo richiamo

Scarica