Documentazione API
Confronta tariffe, crea spedizioni, gestisci ritiri e tracking direttamente dal tuo gestionale, e-commerce o backend.
Introduzione
L'API pubblica di Yellohu permette di integrare quotazione, creazione spedizioni, ritiri e tracking direttamente nel tuo gestionale, e-commerce o backend, senza passare dalla dashboard.
Tutte le chiamate usano JSON su HTTPS, base URL https://yellohu.com/api/v1. Le chiavi si creano e gestiscono dalla dashboard, sezione Integrazioni → API.
Ogni sezione qui sotto ha una console interattiva: incolla la tua chiave Sandbox e prova la chiamata per davvero, direttamente da questa pagina.
Vuoi usarla con un assistente AI invece che scrivere codice? Vedi la documentazione MCP.
Vedi la documentazione MCP →Autenticazione
Ogni richiesta deve includere l'header X-Api-Key con la tua chiave API. Le chiavi hanno due ambienti distinti: yh_test_... per Sandbox (nessun addebito, nessuna spedizione reale) e yh_live_... per Produzione.
Non esiste un ambiente Sandbox separato lato server: la stessa base URL risponde sia alle chiavi test sia a quelle live. È il prefisso della chiave a determinare il comportamento: una chiave yh_test_ non può mai generare un addebito reale né una prenotazione corriere reale, per costruzione.
Puoi avere al massimo una chiave attiva per ambiente. Se ne generi una nuova, la precedente smette di funzionare immediatamente: gestiscile dalla dashboard.
Sandbox e produzione
In Sandbox (chiave yh_test_...), POST /shipments e POST /pickups non chiamano mai un corriere reale né addebitano denaro reale: rispondono in modo deterministico con un numero di tracking fittizio (TEST-...) e un'etichetta segnaposto.
GET /rates e GET /pickup-points sono reali anche in Sandbox: tariffe e punti di ritiro veri, per testare l'integrazione con dati veritieri prima di passare in produzione.
In Produzione (chiave yh_live_...) le spedizioni create tramite API seguono lo stesso flusso di quelle create da checkout: l'ordine viene creato e addebitato, ma la prenotazione con il corriere avviene lato Yellohu (non sincrona). L'etichetta arriva via email o webhook quando pronta, non nella risposta della chiamata.
Pagamenti
Prima di creare spedizioni in produzione devi impostare un metodo di pagamento predefinito dalla dashboard (Integrazioni → API → Pagamento API): carta salvata, gettoni, oppure fatturazione differita se il tuo account è abilitato.
Senza un metodo configurato, POST /shipments con payment:"now" risponde 402 no_payment_method prima ancora di creare l'ordine.
Con metodo "deferred" (fatturazione differita) l'ordine non viene mai addebitato subito: entra nella fatturazione consolidata mensile, esattamente come gli ordini creati da checkout con lo stesso metodo. La risposta ha charged: null e billing_mode: "deferred".
Idempotenza
POST /shipments supporta l'header Idempotency-Key. Se la tua rete ha un timeout e non sai se la richiesta è arrivata, puoi ripeterla identica con la stessa chiave: se l'ordine esiste già, ricevi la stessa risposta invece di creare (e addebitare) una seconda spedizione.
Usa un valore univoco per ogni nuovo ordine (es. un UUID generato lato tuo): non il tuo merchantReference, che può ripetersi per altri motivi.
curl -X POST https://yellohu.com/api/v1/shipments \
-H "X-Api-Key: yh_test_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ceea-467e-b7b1-6d9f5a4c1a2b" \
-d '{ ... }'Limiti di frequenza
I limiti sono per singola chiave API, non per IP. Se superati, ricevi 429 con { "error": { "code": "rate_limited" } }.
| Livello | Limite | Endpoint |
|---|---|---|
| Lettura | 60 richieste / minuto | GET /shipments, GET /shipments/{id}, GET /shipments/{id}/tracking, GET /pickups/{id}, GET /pickup-points, GET /optional-services |
| Scrittura | 20 richieste / minuto | POST /shipments, POST /rates, POST /pickups |
Errori
Ogni errore ha la stessa forma: { "error": { "code": "...", "message"?: "..." } }. Il campo code è sempre presente ed è pensato per essere gestito in modo programmatico; message (quando presente) è pensato per essere leggibile da una persona.
| Codice | HTTP | Significato |
|---|---|---|
| missing_api_key | 401 | Header X-Api-Key assente. |
| invalid_api_key | 401 | Chiave non valida, revocata o sostituita. |
| rate_limited | 429 | Superato il limite di frequenza per questa chiave. |
| invalid_request | 400 | Corpo della richiesta non valido o campo obbligatorio mancante. |
| invalid_locality | 400 | CAP e città non corrispondono (validazione indirizzi italiani). |
| optional_not_available | 400 | Optional richiesto non disponibile per questo corriere/servizio. |
| invalid_optional_value | 400 | Valore fornito per un optional non valido (es. data FDD fuori dalla finestra disponibile). |
| missing_locker_code | 400 | Servizio InPost locker senza recipient.lockerCode. |
| cargo_not_shippable | 422 | La merce descritta non può essere spedita. |
| not_insurable | 422 | La merce descritta non è assicurabile con il canale applicabile. |
| no_payment_method | 402 | Nessun metodo di pagamento predefinito configurato. |
| deferred_not_eligible | 403 | Fatturazione differita richiesta ma l'account non è abilitato. |
| payment_failed | 402 | Addebito rifiutato o fallito. |
| processing | 409 | Richiesta idempotente ancora in corso, riprova (vedi header Retry-After). |
| not_found | 404 | Risorsa inesistente o non tua. |
| pickup_failed | 400 | Richiesta di ritiro non valida. |
| cancellation_not_supported | 409 | Spedizione già presa in carico dal corriere: non annullabile via API. |
| cannot_cancel | 400 | Spedizione in uno stato che non ammette annullamento. |
| invalid_courier | 400 | Corriere sconosciuto nel parametro courier. |
| internal_error | 500 | Errore interno, riprova o contatta il supporto. |
Endpoint
/api/v1/ratesQuotazione tariffe
Calcola le tariffe reali di tutti i corrieri disponibili per una tratta e un collo, già scontate secondo il tuo listino.
Corpo
| Field | Type | Obbligatorio | Descrizione |
|---|---|---|---|
| origin | object | sì | { cap, city, country } |
| destination | object | sì | { cap, city, country } |
| packages | array | sì | Da 1 a 50 colli: { weight (kg), length, width, height (cm) } |
| shipmentType | string | no | "pacchi" (default), "pallet" o "documenti". Un pallet quotato come "pacchi" viene prezzato come un collo fuori misura (margine e supplementi sbagliati): usa "pallet" per basi 80-120cm e/o oltre 100kg. |
| optionals | object | no | Es. { insurance: { value: 200 } }. Alcuni optional richiedono un valore (es. { fixedDeliveryDate: { date: "2026-07-22" } } per DHL) — il prezzo reale di ciascun optional selezionabile è in available_optionals_detail nella risposta. |
Errori possibili
/api/v1/ratesRichiesta
{ "origin": { "cap": "20121", "city": "Milano", "country": "IT" }, "destination": { "cap": "00100", "city": "Roma", "country": "IT" }, "packages": [{ "weight": 5, "length": 30, "width": 20, "height": 15 }] }
Risposta
{ "rates": [ { "courier": "BRT", "service": "express", "price": 8.90, "transit_days": 1, "available_optionals": ["insurance", "tailLiftPickup"], "available_optionals_detail": [ { "key": "insurance", "label": "Assicurazione", "price": 4.90, "requires": "goods_value" }, { "key": "tailLiftPickup", "label": "Sponda idraulica ritiro", "price": 12.00 } ], "breakdown": { "transport": 8.90, "surcharges": [], "optionals": [] } } ] }
Prova questa chiamata
/api/v1/optional-servicesCatalogo optional
Elenca gli optional che influenzano davvero il prezzo per ciascun corriere — sia quelli cotti dal corriere nella tariffa base (assicurazione, sponda idraulica, ecc.) sia quelli sommati additivamente sopra il prezzo base (es. DHL fixedDeliveryDate/preAdvice, quando abilitati) — solo quelli che appaiono anche in available_optionals di POST /rates per quel corriere, mai un optional che poi risulterebbe non prezzabile in una quotazione reale.
Query
| Field | Type | Obbligatorio | Descrizione |
|---|---|---|---|
| courier | string | no | Es. brt: restituisce un array piatto per quel corriere. Se omesso, restituisce il catalogo raggruppato per ogni corriere (vedi risposta). |
Errori possibili
/api/v1/optional-servicesRisposta
{ "optional_services": { "brt": [ { "key": "insurance", "label": "Assicurazione", "courier": "brt" }, { "key": "tailLiftPickup", "label": "Sponda idraulica ritiro", "courier": "brt" }, { "key": "tailLiftDelivery", "label": "Sponda idraulica consegna", "courier": "brt" }, { "key": "nonStackable", "label": "Non sovrapponibile", "courier": "brt" } ], "dhl": [ { "key": "tailLiftPickup", "label": "Sponda idraulica ritiro", "courier": "dhl" }, { "key": "tailLiftDelivery", "label": "Sponda idraulica consegna", "courier": "dhl" }, { "key": "nonStackable", "label": "Non sovrapponibile", "courier": "dhl" } ], "fedex": [ { "key": "insurance", "label": "Assicurazione", "courier": "fedex" }, { "key": "nonStackable", "label": "Non sovrapponibile", "courier": "fedex" } ], "poste": [ { "key": "insurance", "label": "Assicurazione", "courier": "poste" } ], "ups": [], "inpost": [], "palletways": [ { "key": "insurance", "label": "Assicurazione", "courier": "palletways" } ] } }
Prova questa chiamata
/api/v1/shipmentsCrea spedizione
Crea una spedizione. Con payment:"now" (default) l'ordine viene addebitato subito secondo il tuo metodo di pagamento predefinito; con payment:"later" crea solo una bozza da pagare in seguito.
Corpo
| Field | Type | Obbligatorio | Descrizione |
|---|---|---|---|
| payment | "now" | "later" | no | Default "now". |
| courier | string | sì | Es. "brt", case-insensitive. |
| service | string | sì | Es. "express". |
| sender | object | sì | name, street, city, postalCode, countryCode (min 2), + company/province/phone/email opzionali. |
| recipient | object | sì | Come sender. lockerCode solo qui, per InPost. |
| packages | array | sì | Da 1 a 50 colli: weight (kg), length/width/height (cm). |
| shipmentType | string | no | "pacchi" (default), "pallet" o "documenti". Un pallet creato come "pacchi" viene prenotato come un collo fuori misura (margine e supplementi sbagliati). "documenti" esenta questa spedizione da shipmentPurpose/harmonizedCode per le destinazioni extra-UE (nessun valore commerciale). |
| content | string | sì | Descrizione della merce, 2–2000 caratteri. |
| merchandiseValue | number | sì | Valore dichiarato della merce in EUR. |
| pickupDate | string | sì | Obbligatorio salvo courier "inpost". |
| pickupTimeSlot | "AM" | "PM" | "AMPM" | no | Default "AMPM". |
| customs | object | no | Obbligatorio per alcune destinazioni extra-UE (vedi sotto), salvo shipmentType:"documenti". |
| optionals | object | no | Optional selezionati, vedi GET /optional-services. Alcuni richiedono un valore (es. { fixedDeliveryDate: { date: "2026-07-22" } } per DHL, { insurance: { value: 200 } }). |
| merchantReference | string | no | Il tuo riferimento ordine interno (max 255 caratteri), restituito in ogni risposta. |
Errori possibili
/api/v1/shipmentsRichiesta
{ "payment": "now", "courier": "brt", "service": "express", "sender": { "name": "ACME Srl", "street": "Via Dante 1", "city": "Milano", "postalCode": "20121", "countryCode": "IT", "email": "spedizioni@acme.it" }, "recipient": { "name": "Mario Rossi", "street": "Via Roma 1", "city": "Roma", "postalCode": "00100", "countryCode": "IT" }, "packages": [{ "weight": 5, "length": 30, "width": 20, "height": 15 }], "content": "Abbigliamento", "merchandiseValue": 50, "pickupDate": "2026-07-15", "merchantReference": "ORD-10234" }
Risposta
{ "order_number": "O12433", "shipment_id": "S48213", "status": "created", "tracking_number": null, "label_url": null, "charged": { "amount": 10.86, "method": "saved_card" }, "merchant_reference": "ORD-10234", "breakdown": { "transport": 8.90, "surcharges": [], "optionals": [] } }
Prova questa chiamata
/api/v1/shipmentsElenca spedizioni
Elenca le spedizioni create con questa chiave, paginate.
Query
| Field | Type | Obbligatorio | Descrizione |
|---|---|---|---|
| limit | number | no | Default 20, massimo 100. |
| offset | number | no | Default 0. |
/api/v1/shipmentsRisposta
{ "shipments": [ { "shipment_id": "S102", "status": "managed", "tracking_number": "BRT123", "courier": "BRT", "service": "Express", "merchant_reference": "ORD-10234" } ], "has_more": false }
Prova questa chiamata
/api/v1/shipments/{id}Dettaglio spedizione
Stato, tracking number ed etichetta di una spedizione.
Errori possibili
/api/v1/shipments/{id}Risposta
{ "shipment_id": "S48213", "status": "managed", "tracking_number": "BRT123456", "label_url": "https://yellohu.com/.../label.pdf", "merchant_reference": "ORD-10234" }
Prova questa chiamata
/api/v1/shipments/{id}Annulla spedizione
Annulla una spedizione, solo se non è ancora stata presa in carico dal corriere.
Errori possibili
/api/v1/shipments/{id}Risposta
{ "status": "cancelled" }
Prova questa chiamata
/api/v1/shipments/{id}/trackingEventi di tracking
Cronologia eventi di tracking già registrati (nessuna interrogazione live al corriere).
Errori possibili
/api/v1/shipments/{id}/trackingRisposta
{ "events": [ { "status": "picked_up", "occurred_at": "2026-07-11T09:12:00Z", "location": "Milano MI" }, { "status": "delivered", "occurred_at": "2026-07-12T14:03:00Z", "location": "Roma RM" } ] }
Prova questa chiamata
/api/v1/pickupsRichiedi ritiro
Richiede al corriere il ritiro per una spedizione già creata.
Corpo
| Field | Type | Obbligatorio | Descrizione |
|---|---|---|---|
| shipmentId | string | sì | Id della spedizione (es. "S48213"). |
| date | string | no | Data ritiro desiderata, se diversa da quella indicata alla creazione. |
Errori possibili
/api/v1/pickupsRichiesta
{ "shipmentId": "S48213" }
Risposta
{ "status": "booked", "pickup_date": "2026-07-15" }
Prova questa chiamata
/api/v1/pickups/{id}Stato ritiro
Stato del ritiro associato a una spedizione (id = shipment id).
Errori possibili
/api/v1/pickups/{id}Risposta
{ "status": "booked", "pickup_date": "2026-07-15" }
Prova questa chiamata
/api/v1/pickup-pointsCerca punti di ritiro
Cerca locker e punti di ritiro/consegna vicino a un CAP o indirizzo.
Query
| Field | Type | Obbligatorio | Descrizione |
|---|---|---|---|
| postalCode | string | no | Uno tra postalCode e address è obbligatorio. |
| address | string | no | Indirizzo libero, alternativo a postalCode. |
| countryCode | string | no | Default "IT". |
| radius | number | no | Raggio di ricerca in metri. |
| courier | string | no | Filtra per corriere. |
| limit | number | no | Numero massimo di risultati. |
Errori possibili
/api/v1/pickup-pointsRisposta
{ "points": [ { "courier": "INPOST", "point_id": "KRA010M", "type": "locker", "name": "Locker Milano Centrale", "address": { "street": "Piazza Duca d'Aosta", "city": "Milano", "postal_code": "20124", "country_code": "IT" }, "is_247": true, "distance_meters": 320 } ], "source": "live" }
Prova questa chiamata