Track your parcels natively in Home Assistant — InPost, DPD, FedEx, Pocztex, DHL, Orlen Paczka and Allegro One — ready-to-pickup, in-transit and archive, as first-class entities.
- 📥 Do odbioru (InPost) — how many parcels are ready to pick up, with sender, locker, pickup code, expiry and QR payload in attributes
- 🚚 W drodze (both carriers) — parcels on the way, with human-readable Polish statuses
- 🗄️ Archiwum / dostarczone — recently delivered / closed parcels (capped, configurable)
- 👥 Multi-account — add several accounts per carrier, each as its own device
- 🤝 App-to-app sharing (InPost only) — hand a ready parcel to another configured InPost account (a paired "friend"), on a button press or automatically
- 🔑 Official mobile/web auth, no scraping — InPost signs in on its own page in your browser (OAuth2 + PKCE, you paste back the final address); DPD/DHL use SMS login (DHL's stores a session-cookie snapshot instead of a refresh token, verified to survive a real sliding 30-min window). Pocztex is the exception: its Keycloak session has a hard 30-minute cap that no amount of refreshing can extend, so its config entry stores the account password and re-logs-in every poll instead
- 🧩 Almost dependency-free — stdlib
urllibclient for both carriers; the only requirement issegno, for rendering InPost pickup QR codes - 🏷️ Every sensor wears its carrier's own logo — Home Assistant allows one brand image per integration, so all five carriers would otherwise share one generic parcel box; each carrier's sensors carry the carrier badge as
entity_pictureinstead - 🎯 One framework, one entity shape per carrier — adding a new carrier module doesn't touch the others
Every carrier authenticates and polls on its own terms — the divergence in
the left two columns above is real, not simplified away (Under the hood
below has the specifics). They all land on the same dispatch point in
sensor.py, which is exactly the seam that broke twice in this project's
history (an un-dispatched carrier crashing on the first real DPD account,
then again for DHL) — every new carrier module has to be wired in there or
it inherits InPost's entity shape by accident.
Illustrative mock-up — placeholder senders and codes, not a real account.
| Entity | State | Key attributes |
|---|---|---|
sensor · Do odbioru |
number ready to pick up — own + shared with me | do_odbioru_count, w_drodze_count, do_odbioru[] (nadawca, kod odbioru, paczkomat, adres, termin, qr), w_drodze[] |
sensor · W drodze |
number in transit — own parcels only | — |
sensor · Udostępnione |
active parcels shared with this account by someone else | udostepnione[] (numer, nadawca, status, od, kod odbioru, paczkomat, podglad), moje_udostepnione[] (the other direction, with dla), both _counts |
sensor · Archiwum |
number archived | archiwum[] (latest N) |
image · QR do odbioru (×6 slots) |
pickup QR per group (multiskrytka collapses to one) | — |
The
qrpayload lets a Lovelace card render the compartment-opening QR client-side. Eachdo_odbioru[]row also reports its sharing state:wlasciciel(OWN/FRIEND/OBSERVED),udostepniona_doandmozna_udostepnic.
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active (not yet delivered) parcels | active_count, delivered_count, w_drodze[] (numer, nadawca, status, aktualizacja), dostarczone[] (latest N) |
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active (not yet delivered) parcels — own plus parcels shared to this account | active_count, delivered_count, w_drodze[] (numer, nadawca, status, aktualizacja, udostepniona), dostarczone[] (latest N) |
Only one canonical status is confirmed live (TT_DOR → "Dostarczona") — the
research account had a single, already-delivered parcel. Any other status
code shows up as its raw DHL string rather than a guessed translation; see
const.py's DHL section.
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active (not yet delivered) tracking numbers | active_count, delivered_count, w_drodze[] (numer, status, nadawca_miasto, nadawca_kraj, usluga), dostarczone[] (latest N) |
No auto-discovery — FedEx's official Track API is track-by-number only, so the tracked numbers are a manually maintained list in this entry's options, not an account. A number stays tracked (and counted) whether delivered or not until removed from that list.
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active (not yet delivered) parcels | active_count, delivered_count, w_drodze[] (numer, status, postep, aktualizacja), dostarczone[] (latest N) |
Poczta Polska's own state field is already a Polish label — shown as-is,
no canonical-status mapping needed. postep is the raw 0-100 progress
percentage the API returns; anything under 100 counts as active.
Experimental. Status labels come back as free text and are mapped by keyword; they have not been checked against real parcels yet. An unrecognised label keeps the parcel active rather than archiving it.
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active (not yet delivered) tracking numbers | active_count, delivered_count, w_drodze[] (numer, status, aktualizacja), dostarczone[] (latest N) |
Public JSONP track-by-number (no Partner SOAP secrets). Same Options list pattern as FedEx. Details and risk notes: docs/ORLEN_ALLEGRO_ONE.md. Sensors use mdi:truck-delivery (no badge PNG yet).
Experimental. Signs in to the ORLEN Paczka app backend with your phone number and an SMS code, then tracks every parcel addressed to that number; no tracking-number list. The per-parcel field names are best-effort until a real parcel has been seen.
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active parcels | w_drodze[], dostarczone[] (latest N) |
sensor · Do odbioru |
number of parcels ready for pickup | do_odbioru[] |
The integration does not ship the app credentials. Setup asks for the client secret of the ORLEN Paczka app's Entra ID (Azure AD) registration, next to the phone number; it is stored on the config entry only. A missing or rejected secret starts reauthentication, which asks for it again before the SMS step. You supply the secret yourself; this repository does not publish it.
Experimental. Status labels come back as free text and are mapped by keyword; they have not been checked against real parcels yet. An unrecognised label keeps the parcel active rather than archiving it.
| Entity | State | Key attributes |
|---|---|---|
sensor · W drodze |
number of active (not yet delivered) tracking numbers | active_count, delivered_count, w_drodze[] (numer, status, aktualizacja), dostarczone[] (latest N) |
Public edge track-by-number (no Allegro OAuth). Use Allegro-internal waybills (A… / AD…). Undocumented internal Accept header — production risk; see docs/ORLEN_ALLEGRO_ONE.md. Sensors use mdi:truck-delivery (no badge PNG yet).
Configure two InPost accounts and each device gains one entity per other InPost account:
| Entity | What it does |
|---|---|
button · Udostępnij → <alias> |
shares every active parcel not already shared with that account |
switch · Auto-udostępnianie → <alias> |
keeps doing it for each new parcel, on every poll |
Sharing covers every active parcel, in transit included — InPost allows it long before the parcel reaches the locker, so the peer follows the whole journey and gets the pickup code the moment it exists.
The recipient's account then lists those parcels normally — including pickup code and QR — so their sensors, QR image entities and cards need no extra wiring.
The three InPost counters are designed not to overlap, so two mirrored accounts never report the same parcel twice:
Do odbioru = own ready + shared-with-me ready
W drodze = own in transit
Udostępnione = shared with me, active
Prerequisites and limits:
- The two InPost accounts must already be paired in the InPost mobile app (Settings → Sparuj użytkownika, invitation code). Pairing cannot be done over this API; until it is done, both entities stay unavailable.
- InPost decides per parcel whether sharing is allowed (
operations.canShareParcel); parcels it refuses are skipped. - Sharing is not undone here. Turning the switch off stops new shares; it does not withdraw parcels already shared. Withdrawing is an app-side action.
- Adding a further account needs no manual step: the new one gets its sharing entities straight away, and every already-running account is reloaded once so it gains the entities aimed at the newcomer. Removing an account takes those entities back from its peers the same way.
- DPD has no equivalent app-to-app sharing API — the button/switch entities only ever appear on InPost devices.
- HACS → ⋮ → Custom repositories → add
https://github.com/jrx-code/hassio-integration-shipment-trackingas Integration — or just click the Open in HACS badge above. - Install Shipment Tracking (InPost, DPD, FedEx, Pocztex, DHL, Orlen Paczka, Allegro One), then restart Home Assistant.
Copy custom_components/shipment_tracking/ into your Home Assistant config/custom_components/ and restart.
Settings → Devices & Services → Add Integration → Shipment Tracking, then pick a carrier:
InPost:
1. Alias → e.g. "Home"
2. Sign in → open the link shown, sign in on InPost's page
(phone or e-mail), copy the address the browser ends on
(account.inpost-group.com/callback?code=...) and paste it
DPD:
1. Alias → optional
2. Phone → 9 digits
3. SMS code → sent to that number (DPD Mobile)
DHL:
1. Alias → optional
2. Phone → 9 digits
3. SMS code → sent to that number (Mój DHL)
FedEx:
1. Alias → optional
2. API Key → Client ID from a project in your FedEx developer org
3. Secret Key → Client Secret
4. Account number → optional, informational
(tracking numbers are added afterwards, in Options)
Pocztex:
1. Alias → optional
2. E-mail → existing Pocztex Mobile account (app-only registration)
3. Password
Orlen Paczka / Allegro One (same shape as FedEx track-by-number):
1. Alias → optional
(tracking numbers are added afterwards, in Options — see docs/ORLEN_ALLEGRO_ONE.md)
When a session expires, Home Assistant starts a re-auth for that carrier — the browser sign-in again for InPost, a fresh SMS for DPD/DHL, the password form again for Pocztex.
InPost sign-in, details. InPost retired its SMS login on 2026-10-12; the app now signs in with OAuth2 + PKCE behind a Cloudflare captcha and only redirects to its own callback page, so Home Assistant cannot finish the login by itself. Paste the address right away, the code expires within minutes. For several InPost accounts use a private/incognito window per account (InPost's page remembers the signed-in account). Accounts added with the old SMS login ask for this sign-in once after the update; entities and their ids stay as they were. The phone number is read from InPost's token or, when the token has none (e-mail sign-in may give one), from the receiver of the account's own parcels; a re-auth only accepts the same account. Signing in with e-mail (code or password) ends on the same callback page as the phone + SMS sign-in, so the paste step is identical. Per-entry options: polling interval (default 15 min), archived/delivered-parcels cap; FedEx / Orlen Paczka / Allegro One additionally have their tracking-number list there, InPost an ignored-shipment-numbers list (hides a stuck/zombie record InPost's own app stopped showing — see Entities above).
- InPost OAuth2 + PKCE (
client_id=inpost-mobile): browser sign-in onaccount.inpost-group.com(Cloudflare Turnstile), code and refresh atPOST /global/oauth2/tokenon the mobile API host. The access token is cached until shortly before it expires; a rotated refresh token is written back to the entry. The legacy SMS endpoints (/v1/account,/v1/authenticate) answer 404 since 2026-10-12. - DPD SMS auth via Keycloak (
dpdsso.dpd.com.pl, realmDPD) against the DPD Mobile PL backend — not the GEOPOST myDPD/email+password platform. - DHL SMS auth behind an Altcha proof-of-work captcha, solved client-side (brute-force
SHA-256(salt+n)==challenge, a fraction of a second — not an image to click through). Session lives in httpOnly cookies set at login, refreshed with a genuine sliding 30-minute window on every poll. - Pocztex email+password auth via Keycloak authorization_code+PKCE (
idm.pocztex.pl, realmppsa) — direct password grant is disabled for this client, so it drives the same browser-less PKCE dance a login page would. Its session has a hard, non-sliding 30-minute cap: refreshing a token doesn't extend it, so this carrier re-logs-in every poll instead (the config entry stores the account password for that, not just a refresh token). - FedEx uses the official
developer.fedex.comTrack API (OAuth2 client_credentials) — the one carrier here that isn't a reverse-engineered consumer app. - Orlen Paczka uses the public JSONP status endpoint (no Partner SOAP). Allegro One uses
edge.allegro.plwith an internal Accept media type — fragile by design; seedocs/ORLEN_ALLEGRO_ONE.md. - ETag pagination on InPost's
/v4/parcels/tracked— InPost (ab)usesETag/If-None-Matchas a page cursor; a naive single GET misses recent parcels. - Blocking
urllibclients for every carrier, driven from Home Assistant's executor; InPost304responses keep the last snapshot. - Carrier badges are 256² PNGs served by the integration itself at
/shipment_tracking/logo/<carrier>.pngand pointed at byentity_picture. The frontend crops a picture to a circle, so each badge puts the mark on the carrier's own colour with the mark inscribed in that circle — a bare wordmark would be cut down to its middle stripe. - Every carrier's unique_ids and device identifiers are carrier-prefixed
(
inpost_<phone>_...,dpd_<phone>_...,dhl_<entry_id>_..., ...) so the same phone number used on two carriers never collides.
Next carriers, in order: UPS, DHL by number (official Shipment Tracking - Unified
API) and GLS (#6),
then an optional aggregator for cross-border parcels (AliExpress, Temu, Shein). Why these,
the sources, and what was ruled out (Amazon, Vinted Go, Packeta, Cainiao lockers, X-press):
docs/CARRIER_ROADMAP.md. Delivery windows on the sensors:
#7.
Same shape as FedEx: the user's own developer app credentials, tracking numbers in Options, no account auto-discovery (UPS has no consumer "my parcels" API).
What UPS's official OpenAPI specs say (UPS-API/api-documentation,
OAuthClientCredentials.yaml, Tracking.yaml):
- Token:
POST https://onlinetools.ups.com/security/v1/oauth/token, HTTP Basic with the Client ID and Secret,grant_type=client_credentials. The spec describes this flow for an integration owner who is also the UPS shipper; thex-merchant-idheader (6-digit account number) is optional. - Track:
GET https://onlinetools.ups.com/api/track/v1/details/{inquiryNumber}, one number per request (FedEx takes 30), required headerstransIdandtransactionSrc, optionallocale. A test environment exists atwwwcie.ups.com. - Retention: data rolls off after 120 days.
- Delivery windows:
deliveryTimecarries an estimated, confirmed or imminent window (EDW,CDW,IDW, withstartTime/endTime), anddeliveryDatethe scheduled or rescheduled date (SDD,RDD). Direct input for #7.
Steps:
- Spike, go/no-go. Create an app on developer.ups.com and check (a) whether a free UPS
account is enough, without a shipper account number, and (b) whether Track returns an
inbound
1Znumber shipped by someone else. The specs state neither. If (b) fails, stop: the module would be useless to a recipient. api_ups.py: stdliburllibclient run in the executor; token cached until it expires; one GET per number;404means "not in the system yet", not an error.- Status mapping:
ups_canonical()inconst.py, fromcurrentStatusonto the shared buckets, built from UPS's Tracking appendix. Unknown codes fall back to a keyword scan and stay active, the rule DHL and InPost already follow. - Coordinator and sensor modelled on FedEx, plus
okno_doreczenia_od/okno_doreczenia_doandprzewidywana_datafromdeliveryTime/deliveryDate. - Config flow step
ups(alias, Client ID, Client Secret), tracking numbers in Options, translations, a UPS badge inlogos/. - Polling cost: one call per number per interval, so a number in a final state is no longer polled, and numbers past the 120-day retention are dropped.
- Tests and docs: normalizer fixtures from the spec examples and from the spike's responses (redacted); Entities, Configuration and Under the hood sections here.
Unofficial integration, not affiliated with or endorsed by InPost, DPD, FedEx, DHL, Poczta Polska/Pocztex, Orlen Paczka, or Allegro. For InPost/DPD/Pocztex/DHL it talks to each carrier's consumer mobile/web API on your behalf using your own account; FedEx uses their official, documented developer API instead; Orlen Paczka uses a public JSONP status endpoint; Allegro One uses an undocumented public edge endpoint (operator accepts breakage risk — see docs/ORLEN_ALLEGRO_ONE.md). Use it at your own discretion. All carrier names and logos belong to their respective owners.
MIT © JI ENGINEERING