API Documentation
Compare rates, create shipments, and manage pickups and tracking directly from your own system, storefront, or backend.
Introduction
The Yellohu public API lets you integrate rate quoting, shipment creation, pickups, and tracking directly into your own system, storefront, or backend, with no need to go through the dashboard.
Every call uses JSON over HTTPS, base URL https://yellohu.com/api/v1. Keys are created and managed from the dashboard, under Integrazioni → API.
Every section below has an interactive console: paste your Sandbox key and try the call for real, right from this page.
Want to use it with an AI assistant instead of writing code? See the MCP documentation.
See the MCP documentation →Authentication
Every request must include the X-Api-Key header with your API key. Keys come in two distinct environments: yh_test_... for Sandbox (no real charges, no real shipments) and yh_live_... for Production.
There is no separate Sandbox server: the same base URL responds to both test and live keys. The key's prefix determines the behavior: a yh_test_ key can never trigger a real charge or a real courier booking, by construction.
You can have at most one active key per environment. Generating a new one immediately invalidates the previous one: manage them from the dashboard.
Sandbox & production
In Sandbox (yh_test_... key), POST /shipments and POST /pickups never call a real courier or charge real money: they respond deterministically with a fake tracking number (TEST-...) and a placeholder label.
GET /rates and GET /pickup-points are real even in Sandbox: real rates and real pickup points, so you can test your integration against realistic data before going live.
In Production (yh_live_... key), shipments created via the API follow the same flow as checkout-created ones: the order is created and charged, but courier booking happens on Yellohu's side (not synchronously). The label arrives by email or webhook once ready, not in the call's response.
Payments
Before creating shipments in production you need to set a default payment method from the dashboard (Integrazioni → API → Pagamento API): a saved card, gettoni (Yellohu credits), or deferred billing if your account is enabled for it.
Without a configured method, POST /shipments with payment:"now" returns 402 no_payment_method before the order is even created.
With the "deferred" method, the order is never charged immediately: it enters the monthly consolidated invoice, exactly like checkout-created orders using the same method. The response has charged: null and billing_mode: "deferred".
Idempotency
POST /shipments supports the Idempotency-Key header. If your network times out and you're not sure the request went through, you can retry it identically with the same key: if the order already exists, you get back the same response instead of creating (and charging) a second shipment.
Use a unique value for every new order (e.g. a UUID you generate): not your merchantReference, which may repeat for other reasons.
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 '{ ... }'Rate limits
Limits apply per API key, not per IP. If exceeded, you get 429 with { "error": { "code": "rate_limited" } }.
| Tier | Limit | Endpoints |
|---|---|---|
| Read | 60 requests / minute | GET /shipments, GET /shipments/{id}, GET /shipments/{id}/tracking, GET /pickups/{id}, GET /pickup-points, GET /optional-services |
| Write | 20 requests / minute | POST /shipments, POST /rates, POST /pickups |
Errors
Every error has the same shape: { "error": { "code": "...", "message"?: "..." } }. The code field is always present and meant to be handled programmatically; message (when present) is meant to be human-readable.
| Code | HTTP | Meaning |
|---|---|---|
| missing_api_key | 401 | The X-Api-Key header is missing. |
| invalid_api_key | 401 | Key is invalid, revoked, or has been replaced. |
| rate_limited | 429 | Rate limit exceeded for this key. |
| invalid_request | 400 | Invalid request body or missing required field. |
| invalid_locality | 400 | Postal code and city don't match (Italian address validation). |
| optional_not_available | 400 | Requested optional service isn't available for this courier/service. |
| invalid_optional_value | 400 | The value provided for an optional isn't valid (e.g. an FDD date outside the available window). |
| missing_locker_code | 400 | InPost locker service without recipient.lockerCode. |
| cargo_not_shippable | 422 | The described cargo cannot be shipped. |
| not_insurable | 422 | The described cargo isn't insurable via the applicable channel. |
| no_payment_method | 402 | No default payment method configured. |
| deferred_not_eligible | 403 | Deferred billing requested but the account isn't enabled for it. |
| payment_failed | 402 | Charge was declined or failed. |
| processing | 409 | An idempotent request is still in flight, retry (see the Retry-After header). |
| not_found | 404 | Resource doesn't exist or doesn't belong to you. |
| pickup_failed | 400 | Invalid pickup request. |
| cancellation_not_supported | 409 | Shipment already handed to the courier: can't be cancelled via API. |
| cannot_cancel | 400 | Shipment is in a state that doesn't allow cancellation. |
| invalid_courier | 400 | Unknown courier in the courier parameter. |
| internal_error | 500 | Internal error, retry or contact support. |
Endpoints
/api/v1/ratesRate quoting
Computes real rates from every available courier for a route and parcel, already discounted according to your price list.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| origin | object | yes | { cap, city, country } |
| destination | object | yes | { cap, city, country } |
| packages | array | yes | 1 to 50 parcels: { weight (kg), length, width, height (cm) } |
| shipmentType | string | no | "pacchi" (default), "pallet", or "documenti". A pallet quoted as "pacchi" is priced as an oversized parcel (wrong margin and surcharges): use "pallet" for an 80-120cm base and/or 100kg+. |
| optionals | object | no | E.g. { insurance: { value: 200 } }. Some optionals require a value (e.g. { fixedDeliveryDate: { date: "2026-07-22" } } for DHL) — the real price of each selectable optional is in the response's available_optionals_detail. |
Possible errors
/api/v1/ratesRequest
{ "origin": { "cap": "20121", "city": "Milano", "country": "IT" }, "destination": { "cap": "00100", "city": "Roma", "country": "IT" }, "packages": [{ "weight": 5, "length": 30, "width": 20, "height": 15 }] }
Response
{ "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": [] } } ] }
Try this call
/api/v1/optional-servicesOptional services catalog
Lists the optionals that actually affect price per courier — both the ones the courier bakes into its base rate (insurance, tail lift, etc.) and the ones summed additively on top of the base price (e.g. DHL fixedDeliveryDate/preAdvice, when enabled) — only ones that also show up in available_optionals on POST /rates for that courier, never one that would then turn out unpriceable on a real quote. Labels are always returned in Italian (no per-key translation exists yet).
Query
| Field | Type | Required | Description |
|---|---|---|---|
| courier | string | no | E.g. brt: returns a flat array for that courier. If omitted, returns the catalog grouped per courier (see response). |
Possible errors
/api/v1/optional-servicesResponse
{ "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" } ] } }
Try this call
/api/v1/shipmentsCreate shipment
Creates a shipment. With payment:"now" (default) the order is charged immediately via your default payment method; with payment:"later" it only creates a draft to be paid later.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| payment | "now" | "later" | no | Defaults to "now". |
| courier | string | yes | E.g. "brt", case-insensitive. |
| service | string | yes | E.g. "express". |
| sender | object | yes | name, street, city, postalCode, countryCode (min 2), + optional company/province/phone/email. |
| recipient | object | yes | Same as sender. lockerCode only here, for InPost. |
| packages | array | yes | 1 to 50 parcels: weight (kg), length/width/height (cm). |
| shipmentType | string | no | "pacchi" (default), "pallet", or "documenti". A pallet created as "pacchi" is booked as an oversized parcel (wrong margin and surcharges). "documenti" exempts this shipment from shipmentPurpose/harmonizedCode for non-EU destinations (no commercial value). |
| content | string | yes | Cargo description, 2–2000 characters. |
| merchandiseValue | number | yes | Declared value of the goods, in EUR. |
| pickupDate | string | yes | Required unless courier is "inpost". |
| pickupTimeSlot | "AM" | "PM" | "AMPM" | no | Defaults to "AMPM". |
| customs | object | no | Required for some non-EU destinations (see below), unless shipmentType:"documenti". |
| optionals | object | no | Selected optionals, see GET /optional-services. Some require a value (e.g. { fixedDeliveryDate: { date: "2026-07-22" } } for DHL, { insurance: { value: 200 } }). |
| merchantReference | string | no | Your own internal order reference (max 255 chars), echoed back in every response. |
Possible errors
/api/v1/shipmentsRequest
{ "payment": "now", "courier": "brt", "service": "express", "sender": { "name": "ACME Srl", "street": "Via Dante 1", "city": "Milano", "postalCode": "20121", "countryCode": "IT", "email": "shipping@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": "Clothing", "merchandiseValue": 50, "pickupDate": "2026-07-15", "merchantReference": "ORD-10234" }
Response
{ "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": [] } }
Try this call
/api/v1/shipmentsList shipments
Lists shipments created with this key, paginated.
Query
| Field | Type | Required | Description |
|---|---|---|---|
| limit | number | no | Defaults to 20, capped at 100. |
| offset | number | no | Defaults to 0. |
/api/v1/shipmentsResponse
{ "shipments": [ { "shipment_id": "S102", "status": "managed", "tracking_number": "BRT123", "courier": "BRT", "service": "Express", "merchant_reference": "ORD-10234" } ], "has_more": false }
Try this call
/api/v1/shipments/{id}Shipment detail
Status, tracking number, and label for a shipment.
Possible errors
/api/v1/shipments/{id}Response
{ "shipment_id": "S48213", "status": "managed", "tracking_number": "BRT123456", "label_url": "https://yellohu.com/.../label.pdf", "merchant_reference": "ORD-10234" }
Try this call
/api/v1/shipments/{id}Cancel shipment
Cancels a shipment, only if the courier hasn't picked it up yet.
Possible errors
/api/v1/shipments/{id}Response
{ "status": "cancelled" }
Try this call
/api/v1/shipments/{id}/trackingTracking events
History of tracking events already recorded (no live courier query).
Possible errors
/api/v1/shipments/{id}/trackingResponse
{ "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" } ] }
Try this call
/api/v1/pickupsRequest pickup
Requests a courier pickup for an already-created shipment.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| shipmentId | string | yes | Shipment id (e.g. "S48213"). |
| date | string | no | Desired pickup date, if different from the one given at creation. |
Possible errors
/api/v1/pickupsRequest
{ "shipmentId": "S48213" }
Response
{ "status": "booked", "pickup_date": "2026-07-15" }
Try this call
/api/v1/pickups/{id}Pickup status
Status of the pickup associated with a shipment (id = shipment id).
Possible errors
/api/v1/pickups/{id}Response
{ "status": "booked", "pickup_date": "2026-07-15" }
Try this call
/api/v1/pickup-pointsSearch pickup points
Searches lockers and pickup/drop-off points near a postal code or address.
Query
| Field | Type | Required | Description |
|---|---|---|---|
| postalCode | string | no | One of postalCode or address is required. |
| address | string | no | Free-text address, alternative to postalCode. |
| countryCode | string | no | Defaults to "IT". |
| radius | number | no | Search radius in meters. |
| courier | string | no | Filter by courier. |
| limit | number | no | Maximum number of results. |
Possible errors
/api/v1/pickup-pointsResponse
{ "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" }
Try this call