Skip to main content

smartposti-tracker

CI License: MIT PyPI Python

An unofficial Python library and CLI for tracking SmartPosti (Estonia, Latvia, Lithuania) and Posti (Finland) parcels. It also watches parcels and tells you when new tracking events appear.

$ smartposti-track watch JJFI12345678901234567 --interval 300
NEW  JJFI12345678901234567  2026-10-29 06:46  The item is in sorting
NEW  JJFI12345678901234567  2026-10-29 11:02  The item has arrived at the parcel locker

Disclaimer. This project is not affiliated with or endorsed by Posti Group. It uses the same public endpoints as the tracking page on smartposti.ee, and they are not documented. They may change or stop working without notice. Please poll responsibly (every few minutes, not every second).

Features

  • No browser needed. Plain HTTP via requests, with no Selenium or Playwright.
  • Library first. Typed Shipment and Event models, and a small exception hierarchy.
  • Watcher with pluggable parts. You choose where seen events are stored (memory, JSON file, or your own store) and what happens when one arrives (print, Telegram, email, webhook, and so on).
  • CLI for quick lookups, and a --once mode for cron or Windows Task Scheduler.
  • Localized events in en, et, fi, lv, lt.

Installation

pip install smartposti-tracker

For development:

git clone https://github.com/kuttim/smartposti-tracker.git
cd smartposti-tracker
pip install -e ".[dev]"

Requires Python 3.9+.

Command line

# Current status and full history
smartposti-track status JJFI12345678901234567

# Raw API response as JSON
smartposti-track status JJFI12345678901234567 --json

# Poll every 5 minutes and print only new events (Ctrl+C to stop)
smartposti-track watch CODE1 CODE2 --interval 300 --bell

# Check once and exit. Good for cron or Task Scheduler
smartposti-track watch CODE --once --state ~/.smartposti_state.json

# Events in Estonian
smartposti-track --locale et status CODE

python -m smartposti_tracker ... also works.

On the first watch check, existing events are only recorded, not reported. Pass --notify-existing to print them too.

Library usage

Look up a shipment

from smartposti_tracker import TrackingClient, ShipmentNotFound

client = TrackingClient(locale="en")

try:
    shipment = client.get_shipment("JJFI12345678901234567")
except ShipmentNotFound:
    print("No such parcel")
else:
    print(shipment.status)                    # "IN_TRANSPORT"
    for event in shipment.events:             # oldest first
        print(event.timestamp, event.description)
    print(shipment.latest_event)

shipment.raw and event.raw hold the original API data.

Watch for new events

from smartposti_tracker import JsonFileStore, Watcher

watcher = Watcher(store=JsonFileStore("state.json"))

@watcher.add_handler
def on_new_events(shipment, events):
    for e in events:
        print(f"{shipment.tracking_code}: {e.description}")

watcher.run(["JJFI12345678901234567"], interval=600)   # blocks
# or: watcher.check("CODE") -> list of new events, for your own scheduler

Extending

The library has three small, replaceable pieces:

Piece What it does How to replace it
TrackingClient Talks to the API Pass your own requests.Session (proxies, retries), or query= to request more GraphQL fields
StateStore Remembers which events were already seen Any object with get_seen(code) and set_seen(code, keys)
Event handlers React to new events Any callable handler(shipment, new_events)

Custom notifications

A handler is just a function. Here is one that posts to a webhook:

import requests

def to_webhook(shipment, events):
    requests.post("https://example.com/hook", json={
        "code": shipment.tracking_code,
        "status": shipment.status,
        "events": [{"time": e.timestamp.isoformat(), "text": e.description} for e in events],
    })

Watcher(handlers=[to_webhook]).run(["CODE"])

Custom storage

class RedisStore:
    def __init__(self, redis):
        self.r = redis

    def get_seen(self, code):
        # Return None if this code was never checked, so the first check stays silent
        if not self.r.exists(f"seen:{code}"):
            return None
        return {k.decode() for k in self.r.smembers(f"seen:{code}")}

    def set_seen(self, code, keys):
        pipe = self.r.pipeline()
        pipe.delete(f"seen:{code}")
        pipe.sadd(f"seen:{code}", "__checked__", *keys)
        pipe.execute()

Retries and proxies

import requests
from requests.adapters import HTTPAdapter, Retry

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=2)))
client = TrackingClient(session=session)

Requesting more fields

The default query asks only for status and events. To get delivery details, pickup point, and similar data, pass your own GraphQL query that still returns consumerSearchShipments.hits, then read the extra fields from shipment.raw.

See examples/ for complete scripts, including a Telegram notifier and a SQLite store.

Running on a schedule

Linux/macOS (cron), every 10 minutes:

*/10 * * * * smartposti-track watch CODE --once --state $HOME/.smartposti_state.json >> $HOME/parcels.log

Windows (Task Scheduler):

schtasks /Create /SC MINUTE /MO 10 /TN "Parcel tracker" `
  /TR "smartposti-track watch CODE --once --state %USERPROFILE%\smartposti_state.json"

How it works

The smartposti.ee tracking page loads its data in the browser in two steps. This library repeats those two requests:

  1. POST https://auth-service.posti.fi/api/v1/anonymous_token returns a short-lived anonymous id_token and a role_tokens[0].token. No account is needed.
  2. POST https://graphql.posti.fi/graphql runs the consumerSearchShipments query, with authorization: <role token> and x-posti-token: Bearer <id_token>.

Tokens are cached until shortly before they expire. A 401 or 403 response causes a fresh token on the next call.

If Posti changes this flow, open the tracking page with your browser's dev tools open (Network tab, filter graphql) to see the new requests, then open an issue.

Development

pip install -e ".[dev]"
ruff check .
pytest                                         # offline, API is mocked
SMARTPOSTI_TEST_CODE=<code> pytest -m live     # optional real-API smoke test

See CONTRIBUTING.md.

License

MIT

Metadata

Release files for smartposti-tracker 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for smartposti-tracker 0.1.0
File Size Uploaded Provenance Actions
smartposti_tracker-0.1.0.tar.gz 14.8 kB With provenance Details for smartposti_tracker-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for smartposti-tracker 0.1.0
File Interpreter ABI Platform Actions
smartposti_tracker-0.1.0-py3-none-any.whl Python 3 none any Details for smartposti_tracker-0.1.0-py3-none-any.whl

Total release size: 28.4 kB

Release files / smartposti_tracker-0.1.0.tar.gz

Download URL smartposti_tracker-0.1.0.tar.gz
Size 14.8 kB
Tags Source
SHA-256 checksum
How to use checksums
5eb1262e991f53ba6510b6ddf9cf292cb426240508a121c034d81ad23113fa9e
BLAKE2b-256 checksum
How to use checksums
babbfd39db04c555453256378e39a586a9e9ea7669ae933ba53b88a37f5fe607
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Release files / smartposti_tracker-0.1.0-py3-none-any.whl

Download URL smartposti_tracker-0.1.0-py3-none-any.whl
Size 13.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
70efebfb12a9eaf160988dc6b5e022eac15ee187c8aec7a4385b00ce47e0fc83
BLAKE2b-256 checksum
How to use checksums
d0acf1fe29e7fa4cee8bba3e711156603d50c445f3fe6adfa6205e24660e6315
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

Security for smartposti-tracker 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for smartposti_tracker-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 29, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page