Vai al contenuto

Documentazione dell'API REST v1

Host, chiavi, rotte, errori e limiti dell'API di HorusFinder, con esempi in fetch e curl.

Aggiornata a · Le chiavi negli esempi finiscono con «…»: le tue le crei nella console.

Con l'API REST di HorusFinder carichi il catalogo del negozio e cerchi i prodotti, dal browser o dal tuo server. Parla JSON su HTTPS e si usa da qualsiasi linguaggio. Le chiavi le crei nella console di HorusFinder, che apriamo su richiesta.

In breve

Dal browser cerchi con la chiave pubblica (pk), che va in chiaro nella pagina del negozio:

JavaScript
// Dal browser, con la chiave pubblica (pk)
const url = new URL("https://api.horusfinder.com/v1/public/esempio/collections/products_it/search");
url.search = new URLSearchParams({ q: "scarpe da trail", "f.availability": "in_stock", size: "24", key: "hrf_pk_live_…" });
const res = await fetch(url);
if (!res.ok) throw new Error(`HorusFinder ${res.status}`); // qui il tuo ripiego
const { queryId, total, hits } = await res.json();

Dal server carichi il catalogo e cerchi con una chiave segreta (sk), che resta sul server:

curl
# Dal server, con la chiave segreta (sk, scope ingest): carichi o aggiorni il catalogo
curl https://api.horusfinder.com/v1/collections/products_it/documents/batch \
  -H "X-Api-Key: hrf_sk_live_…" -H "Content-Type: application/json" \
  -d '{"upsert":[{"id":"2746","doc":{"name":"Scarpa da trail X","price":89.9,"currency":"EUR","availability":"in_stock","url":"https://shop.example/2746"}}]}'
# Ricerca dal server (sk, scope search)
curl https://api.horusfinder.com/v1/collections/products_it/search \
  -H "X-Api-Key: hrf_sk_live_…" -H "Content-Type: application/json" \
  -d '{"q":"scarpe da trail","filters":{"availability":"in_stock"},"size":24}'

Host e chiavi

AmbienteHostChiavi
Produzionehttps://api.horusfinder.comhrf_pk_live_…, hrf_sk_live_…
Testhttps://api-test.horusfinder.comhrf_pk_test_…, hrf_sk_test_…
  • Solo HTTPS, con TLS 1.2 o superiore. Le richieste passano dalla rete di Cloudflare, senza cache.
  • Una chiave vale solo sull'host del suo ambiente: altrove riceve 401.
  • Il negozio (il tenant) lo decide sempre la chiave, mai il corpo della richiesta.

Chiave pubblica (pk)

  • Per il browser. Va nel parametro key= ed è ammessa solo sulle rotte /v1/public/{tenant}/….
  • Non è un segreto: sta nell'HTML del negozio. Per questo è legata alle origini del negozio, che imposti nella console: indirizzi https://; gli host locali (localhost, 127.0.0.1, [::1]) valgono solo con le chiavi di test, e solo loro anche in http://.
  • Il {tenant} del percorso è lo slug del tuo negozio e deve essere quello della chiave, altrimenti 401.
  • Una richiesta con un Origin che non è fra le origini della chiave, anche null, riceve 403. Una richiesta senza Origin passa.
  • Con la chiave pubblica si leggono solo i campi public dello schema, e i filtri obbligatori della chiave, se ne hai impostati, valgono sempre, in AND con i tuoi.

Chiave segreta (sk)

  • Per il server. Va nell'header X-Api-Key ed è ammessa su tutte le altre rotte.
  • Ha uno o più scope: search, ingest, events. Uno scope mancante dà 403.
  • Non va mai in un browser né in un'app: chi la legge può cambiare il tuo catalogo.
  • In richiesta: X-Api-Key con la chiave segreta; Content-Type: application/json sui corpi dal server; Accept-Encoding: gzip, se vuoi la risposta compressa. Le richieste non si comprimono.
  • In risposta: Cache-Control: no-store sempre; Retry-After, in secondi, su 429 e 503; Content-Encoding: gzip se l'hai chiesto; apigw-requestid, l'identificativo della richiesta da citare quando ci scrivi.
  • CORS: Access-Control-Allow-Origin: *, senza credenziali; Retry-After e apigw-requestid sono leggibili dalla pagina.

Regole comuni

  • Versioni. /v1 cambia solo per aggiunta: campi facoltativi nuovi in richiesta, campi e valori nuovi in risposta. Una modifica che rompe la compatibilità apre /v2, e /v1 resta servita almeno 6 mesi, con un avviso su questa pagina e per email.
  • Campi sconosciuti in risposta: ignorali. Uno status di un elemento che non conosci vale failed, un code che non conosci vale come il suo status.
  • Campi sconosciuti in richiesta: si scartano senza errore. Un filtro su un campo che lo schema non ha si ignora, e con la chiave pubblica anche un filtro su un campo non public. Un ordinamento o una facet su un campo non ammesso danno 400. Un nome che inizia con _ dà sempre 400.
  • Il documento che carichi, invece, è stretto: un campo non dichiarato nello schema fa fallire quell'elemento (vedi Lotti di documenti).
  • Formati. JSON in UTF-8. Gli istanti sono in ISO 8601 UTC con i millisecondi; version è in millisecondi dal 1970. page parte da 0.
  • Identificativi. Documento: ^[A-Za-z0-9._:-]{1,128}$. Collection: ^[a-z][a-z0-9_]{0,30}$. Tenant: ^[a-z][a-z0-9]{2,30}$.
  • Testo e HTML. highlight è l'unico campo in HTML, con <mark> e l'escape già fatto. Tutto il resto, didYouMean e le ricerche del momento compresi, si mostra come testo.
  • Ripetere è sicuro. L'upsert del documento intero con version, il delete per id, il prune e il PUT della collection danno lo stesso stato se ripetuti; un evento conta una volta sola grazie al suo eventId. Non serve un header d'idempotenza.
  • Valori iniziali. I valori di default e alcuni limiti indicati come «iniziali» possono essere ritoccati: ogni modifica si annota qui.

Rotte

RottaChiaveEsito
GET /v1/public/{tenant}/collections/{collection}/searchpk200
GET /v1/public/{tenant}/collections/{collection}/suggestpk200
GET /v1/public/{tenant}/collections/{collection}/trendingpk200
POST /v1/public/{tenant}/eventspk204
POST /v1/collections/{collection}/searchsk, scope search200
PUT /v1/collections/{collection}sk, scope ingest200
GET /v1/collections/{collection}sk, scope ingest200
GET /v1/tenantsk, scope ingest200
POST /v1/collections/{collection}/documents/batchsk, scope ingest200
POST /v1/collections/{collection}/documents/prunesk, scope ingest202
POST /v1/eventssk, scope events200

Ricerca dal browser

I parametri arrivano già decodificati. Le liste si dividono sulla virgola, quindi un valore che contiene una virgola non si filtra con una GET.

q=<testo>                     sotto i 2 caratteri: tutto il catalogo, filtri compresi
f.<campo>=<v>[,<v>…]          OR nel campo, AND fra campi; true/false, numeri col punto
r.<campo>=<min>..<max>        estremi inclusi, anche <min>.. o ..<max>; numeri o date ISO
sort=<campo>[:asc|:desc][,…]  al più 5 chiavi, default desc; senza sort vale la rilevanza
facets=<campo>[:<n>][,…]      al più 5 facet da 50 valori; n di default 10 (valore iniziale)
page=<n>  size=<n>            size al più 48, default 24 (valore iniziale); solo i primi 480 risultati
retrieve=<campo>[,…]          campi da restituire, solo fra i public; senza, tutti i public restituibili
key=hrf_pk_…                  obbligatoria
HTTP
GET /v1/public/esempio/collections/products_it/search?q=scarpe%20trail&f.availability=in_stock&r.price=40..120&facets=manufacturer,price&key=hrf_pk_live_…
Origin: https://shop.example
JSON
{"queryId": "01K79Z4T8Q6M3VXR2C5N7PBJ0D", "total": 37, "page": 0, "size": 24, "tookMs": 48,
 "hits": [{"id": "2746", "score": 12.4,
           "doc": {"name": "Scarpa da trail X", "price": 89.9, "currency": "EUR", "availability": "in_stock", "url": "https://shop.example/2746"},
           "highlight": {"name": ["<mark>Scarpa</mark> da <mark>trail</mark> X"]}}],
 "facets": {"manufacturer": {"values": [{"value": "Marca Uno", "count": 21}, {"value": "Marca Due", "count": 9}]},
            "price": {"values": [{"value": 89.9, "count": 5}], "min": 42.5, "max": 119.9}}}
  • Ogni parola di q deve trovarsi in almeno un campo cercabile, con la tolleranza ai refusi: la ricerca è in AND. Nelle collection product@1 con «disponibili prima» acceso (il default) i prodotti disponibili precedono gli esauriti anche con un sort esplicito.
  • queryId, un identificativo casuale ordinabile nel tempo (ULID), c'è sempre: va negli eventi. total è esatto.
  • score è null con un ordinamento esplicito e sui risultati fissati in cima da una regola, che portano pinned: true. highlight ha al più 3 campi. doc ha i campi restituibili, e con la chiave pubblica solo quelli public.
  • Le facet sono disgiuntive: i conteggi di una facet ignorano il filtro sul suo stesso campo. values è ordinato per conteggio; min e max ci sono solo sui campi numerici.
  • didYouMean (testo) compare solo a pagina 0, con meno di 3 risultati, se la collection lo ha acceso. highlight segue l'impostazione della collection.

Suggerimenti e ricerche del momento

Suggerimenti (…/suggest): q, f.*, retrieve, size (al più 10, default 8) e key; niente facet né didYouMean. Valgono le regole della collection, come nella ricerca.

HTTP
GET /v1/public/esempio/collections/products_it/suggest?q=scar&f.availability=in_stock&key=hrf_pk_live_…
JSON
{"queryId": "01K79Z4W2H8F6S3D1A9G7K5M4N", "tookMs": 22,
 "hits": [{"id": "2746", "doc": {"name": "Scarpa da trail X"}, "highlight": {"name": ["<mark>Scarpa</mark> da trail X"]}}]}

L'evidenziazione copre la parola intera, non solo le lettere digitate.

Ricerche del momento (…/trending): solo key.

JSON
{"queries": [{"q": "scarpe trail"}, {"q": "zaino 30 litri"}], "updatedAt": "2026-10-09T08:05:00.000Z"}
  • Al più 10 termini, senza conteggi, ricalcolati ogni ora al minuto 5 (UTC). La lista è vuota se la collection ha ancora poche ricerche; updatedAt è null finché non è mai stata calcolata.
  • Dalla console puoi bloccare un termine, oppure scegliere tu la lista (modalità curata): in quel caso la risposta è la tua lista e updatedAt è l'ora dell'ultima modifica.

Eventi dal browser

Gli eventi dicono cosa succede dopo una ricerca: servono alle statistiche della console. Il corpo è JSON ma parte come text/plain con navigator.sendBeacon, così il browser non fa la richiesta preliminare CORS. La risposta è 204, senza corpo.

HTTP
POST /v1/public/esempio/events?key=hrf_pk_live_…
Content-Type: text/plain;charset=UTF-8
Origin: https://shop.example

{"events": [{"eventId": "5d2e7f90-1c3b-4a8e-b6d4-2f9a0c1e7b35", "type": "click", "collection": "products_it",
             "sessionId": "8KfQ2xR7mN4pW1sL", "queryId": "01K79Z4T8Q6M3VXR2C5N7PBJ0D", "objectId": "2746", "position": 1}]}
typeCampi propriQuando
search_viewqueryIdla ricerca si ferma per 1 s, con invio o con un clic; solo per la pagina 0, mai a ogni tasto
clickqueryId, objectId, position (da 1)anche su un suggerimento, con il queryId del suggest
viewqueryId, objectIdil visitatore apre il prodotto
conversionqueryId, objectId, value, currency, quantity?value intero in unità minori (centesimi), currency ISO 4217
  • Campi comuni: eventId (UUID), type, collection (quella della risposta che ha dato il queryId), sessionId (casuale, vale per la sola sessione) e clientId, solo se il visitatore ha dato il consenso al negozio. sessionId e clientId sono stringhe base64url di 8–64 caratteri (^[A-Za-z0-9_-]{8,64}$): con un altro formato l'evento non è valido.
  • Mai IP, User-Agent, URL, referrer, id dell'utente o email negli eventi. Dalla console il negozio cancella gli eventi di un clientId, su richiesta del visitatore.
  • position arriva al più a 10.000 e quantity al più a 1.000.000.
  • Un evento entra solo se il suo queryId è dello stesso negozio e della stessa collection, da al più 24 ore. Gli eventi non validi si scartano in silenzio (la risposta resta 204) e un eventId già ricevuto non conta due volte.
  • Errori: 400 per un corpo che non è JSON o senza events; 401; 403 per l'Origin; 413 oltre 50 eventi o 16 KB.

Ricerca dal server

HTTP
POST /v1/collections/products_it/search
X-Api-Key: hrf_sk_live_…
Content-Type: application/json

{"q": "scarpe trail", "filters": {"availability": "in_stock", "category_ids": [12, 15]},
 "ranges": [{"field": "price", "gte": 40, "lte": 120}], "sort": [{"field": "price", "dir": "asc"}], "page": 0, "size": 50,
 "facets": [{"field": "manufacturer", "size": 100}], "retrieve": ["name", "price"], "highlight": false, "tags": ["sito-web"]}
  • La risposta è 200, con la forma della ricerca dal browser. Valgono tutti i campi dello schema, non solo i public.
  • size arriva a 500 (default 24, valore iniziale) e le facet a 20 da 100 valori (default 10).
  • filters vuole un valore o un elenco per campo: OR nel campo, AND fra campi. ranges usa gte, gt, lte, lt. sort ha direzione di default desc. L'evidenziazione c'è solo con highlight: true.
  • tags sono etichette per le statistiche: al più 5 (valore iniziale), nella forma ^[a-z0-9][a-z0-9._-]{0,31}$. Il tag riservato as-you-type segna una ricerca lanciata mentre il visitatore scrive: conta nei consumi ma resta fuori da statistiche e ricerche del momento.

Collection e tenant

PUT /v1/collections/{collection} crea la collection o ne sostituisce lo schema. Il corpo ha kind (il preset: product@1 o custom), locale (it, en o neutral) e schema. Con kind: "product@1" lo schema si omette e vale quello del preset, come in {"kind": "product@1", "locale": "en"}.

Ogni campo dello schema ha name, type (text, keyword, integer, long, float, double, boolean, date, object), array e i flag searchable (true o {"prefix": true}), filterable, facetable, sortable, retrievable e public.

JSON
{"kind": "custom", "locale": "it", "schema": {"fields": [
  {"name": "name", "type": "text", "searchable": {"prefix": true}, "sortable": true, "retrievable": true, "public": true},
  {"name": "visible", "type": "boolean", "filterable": true}]}}
  • La risposta è 200: {"schemaVersion": 4, "state": "active"}, oppure {"schemaVersion": 5, "state": "reindexing", "reindex": true} quando il cambio richiede una reindicizzazione (tipo, locale o prefisso di un campo esistente). Uno schema uguale a quello attivo, o a quello in arrivo, risponde 200 senza lavoro.
  • Pesi, sinonimi, regole e cancellazione di una collection si gestiscono solo dalla console.
  • Una collection che il negozio non ha dà 404, tranne nel PUT, che la crea. Errori del PUT: 409 collection_not_ready per uno schema diverso durante una reindicizzazione, 409 capacity_exceeded per l'undicesima collection.

GET /v1/collections/{collection} legge una collection:

JSON
{"id": "products_it", "kind": "custom", "locale": "it", "state": "active", "schemaVersion": 4, "docCount": 7159,
 "mappedFields": [{"name": "name", "type": "text", "prefix": true}, …]}

Durante una reindicizzazione schemaVersion è la versione attiva e pendingSchemaVersion quella in arrivo: chi aspetta la fine rilegge finché state torna active. docCount vale null se il motore di ricerca non risponde.

GET /v1/tenant dice com'è messo il negozio:

JSON
{"tenant": {"slug": "esempio", "env": "live", "status": "active"},
 "collections": [{"id": "products_it", "kind": "product@1", "locale": "it", "ready": true, "docCount": 5120}]}

env vale live o test, come nelle chiavi; ready è true quando la collection è active.

Lotti di documenti

POST /v1/collections/{collection}/documents/batch, al più 500 elementi e 4 MB per lotto, 64 KB per documento.

JSON
{"upsert": [{"id": "2746", "version": 1791533520000, "doc": {"name": "Scarpa da trail X", "price": 89.9, "currency": "EUR", "availability": "in_stock"}}],
 "delete": [{"id": "1259", "version": 1791533520000}], "refresh": "wait_for"}
JSON
{"receivedAt": "2026-10-09T08:12:03.123Z",
 "items": [{"id": "2746", "status": "ok"}, {"id": "1259", "status": "not_found"}, {"id": "77", "status": "failed", "code": "schema_violation"}]}
  • La risposta è 200 anche con elementi falliti: guarda lo status di ognuno.
  • L'upsert sostituisce il documento intero; il delete è per id, e un id assente dà not_found, che conta come riuscito. Un id ripetuto nello stesso lotto: vale l'ultimo.
  • Senza refresh i documenti diventano cercabili in genere entro uno o due secondi; con "refresh": "wait_for" la risposta arriva quando lo sono.
  • version è il momento in cui hai costruito il documento, in millisecondi, fra 10^12 e adesso più 24 ore; senza, vale l'istante di ricezione. Una versione più vecchia di quella salvata dà stale, che non è un errore: un lotto ripetuto può dire stale dove il primo tentativo diceva ok.
  • failed porta un code: schema_violation (campo non dichiarato o di tipo sbagliato), reserved_field (campo che inizia con _), document_too_large (oltre 64 KB), invalid_version (fuori intervallo). Senza code è un errore imprevisto: riprova quell'elemento in un lotto successivo.
  • Una reindicizzazione in corso, o un sovraccarico durante il lotto, dà 503 con Retry-After all'intera richiesta. Nell'ambiente di test una collection tiene al più 100.000 documenti: oltre, 409 capacity_exceeded. Una richiesta respinta per intero vale «tutti gli elementi falliti».

Prune

POST /v1/collections/{collection}/documents/prune cancella ciò che un caricamento completo non ha più scritto.

JSON
{"seenBefore": "2026-10-09T02:30:04.120Z", "maxDeletePercent": 10}

La risposta è 202: {"total": 7159, "orphans": 12}.

  • Cancella in background i documenti scritti l'ultima volta prima di seenBefore; un prune successivo raccoglie i resti. Uno stale o un failed di un documento già presente conta come scrittura, quindi il prune non lo cancella.
  • Come seenBefore usa il più piccolo dei receivedAt dei lotti del caricamento completo, e salta il prune se un lotto è stato respinto per intero.
  • Se gli orfani superano maxDeletePercent del totale (default 10): 409 con code: "delete_guard", total e orphans. Si ripete con "force": true, ma solo dopo aver controllato i numeri. Durante una reindicizzazione: 409 collection_not_ready.

Eventi dal server

Gli stessi tipi, campi e limiti degli eventi dal browser, con Content-Type: application/json e la chiave segreta con scope events. Dal server sessionId è facoltativo, il queryId vale 30 giorni ed è obbligatorio anche per le conversion: un acquisto si lega alla ricerca con queryId e objectId, senza id dell'utente.

JSON
{"events": [{"eventId": "8f0a2c4e-3b1d-4e6f-9a7c-5d2b8e1f0a63", "type": "conversion", "collection": "products_it",
             "queryId": "01K79Z4T8Q6M3VXR2C5N7PBJ0D", "objectId": "2746", "value": 8990, "currency": "EUR", "quantity": 1}]}

La risposta è 200: {"accepted": 1, "rejected": []}. rejected elenca gli eventi respinti dalla validazione, come [{"index": 0, "error": "value: Invalid input: expected number, received undefined"}]: non vanno ripetuti. accepted conta gli eventi validi nella forma, compresi i doppioni e quelli con un queryId scaduto, che non si salvano. Un corpo che non è JSON, o senza events, dà 400.

Errori

Decide lo status. Il corpo di un errore ha una di queste tre forme:

  • {"error": "<testo>"}. Nei 400 di validazione il testo nomina il campo, come {"error": "size: Too big: expected number to be <=48"}; più problemi si separano con ; . Il testo è in inglese e serve a chi legge: non interpretarlo, perché può cambiare.
  • {"error": "<testo>", "code": "<codice>"}, solo dove devi scegliere fra più casi: i 409 della tabella e gli elementi dei lotti.
  • {"message": "<testo>"} quando risponde il gateway: il 401 di una chiave segreta, il 429 dei limiti di velocità sulle rotte del server, i suoi 5xx.
StatusQuandoCosa fare
400richiesta non valida; ordinamento o facet su un campo non ammesso; risultati oltre il 480° con la chiave pubblica, oltre il 10.000° per tutti; risposta oltre circa 5,5 MBcorreggi la richiesta: non ripeterla e non ripiegare
401chiave assente, sbagliata, revocata, scaduta, di un altro negozio o ambiente; negozio non attivo. Con una chiave segreta anche un guasto temporaneo nella verifica della chiavenon ripetere e ripiega; non cancellare mai la chiave per un 401
403scope mancante; Origin fuori dalle origini della chiavenon ripetere e ripiega
404collection o rotta sconosciutanon ripetere e ripiega
409 collection_not_readyreindicizzazione in corso: prune, oppure PUT di uno schema diversoriprova più tardi; il prune si salta
409 capacity_exceededundicesima collection; nell'ambiente di test, oltre 100.000 documenti in una collectionfermati: serve una pulizia, o scrivici
409 delete_guardil prune cancellerebbe più di maxDeletePercent; il corpo ha total e orphanscontrolla i numeri; forza solo a mano
413corpo oltre i limitidividi il lotto
429troppe richiesteaspetta Retry-After, se c'è; dal browser ripiega
500errore imprevistonon ripetere e ripiega
502, 503, 504motore di ricerca lento o in errore; indice fermo per un attimo mentre si salvano i sinonimi; sovraccarico durante un lotto; scrittura durante una reindicizzazione; servizio che non risponde. Mai risultati parziali: una ricerca è completa o è un erroreun solo tentativo dopo Retry-After; dal browser ripiega

Il ripiego. Su un 400 la richiesta è sbagliata e va corretta. Su ogni altro esito il negozio deve restare usabile: dai un tempo massimo alla chiamata (dal browser bastano pochi secondi) e, se scade o se arriva un errore, mostra la ricerca che il negozio aveva prima. Il modulo PrestaShop fa così: se HorusFinder non risponde, torna da solo alla ricerca di PrestaShop.

Limiti

CosaLimiteOltre
Lotto di documenti; documento500 elementi e 4 MB; 64 KB413; elemento document_too_large
Lotto di eventi50 eventi e 16 KB413
qle prime 16 parole e 256 caratteri (valori iniziali)si tronca senza errore
size; paginechiave pubblica 48, chiave segreta 500, suggerimenti 10; con la chiave pubblica solo i primi 480 risultati, cioè (page + 1) × size al più 480; per tutti i primi 10.000400
Facet; sort; rangeschiave pubblica 5 da 50 valori, chiave segreta 20 da 100; 5 chiavi; 10 intervalli400
Rispostacirca 5,5 MB, contati sulla risposta come parte (compressa, se hai chiesto gzip)400: riduci size o retrieve
Collection per negozio; documenti per collection, solo nell'ambiente di test10; 100.000409 capacity_exceeded
Campi dello schema100400

Limiti di velocità. Sulle rotte /v1/public/ ci sono un limite per indirizzo IP di Cloudflare e limiti per IP e per negozio nel servizio, tarati sopra il traffico di un visitatore che scrive, filtra e scorre i risultati. Il 429 del servizio ha Retry-After leggibile; quello di Cloudflare arriva senza gli header CORS, e il browser lo vede come un errore di rete: trattalo come un errore e ripiega. Le rotte con la chiave segreta hanno un tetto per rotta, il cui 429 ha {message} e nessun Retry-After.

Limiti del piano. Nelle richieste del mese contano le ricerche, i suggerimenti e le ricerche del momento andati a buon fine; eventi, caricamento del catalogo e console non contano. I limiti del piano non danno mai un 429: se li superi te lo diciamo, e la ricerca continua.

Smetti di perdere vendite sulla barra di ricerca.

Richiedi l'accesso

L'accesso è su richiesta: raccontaci il tuo negozio (piattaforma, numero di prodotti, lingue) e ti diciamo con franchezza se HorusFinder fa per te e come si parte.

Scrivici