One Python interface across Shippo, ShipStation and Easyship.
Bring your own provider accounts. Keep your contracts and negotiated rates.
flowchart LR
APP["Your application<br/><code>shipzil.Gateway</code>"]
APP --> SP["Shippo"]
APP --> SS2["ShipStation v2"]
APP --> SS1["ShipStation v1"]
APP --> ES["Easyship"]
SP --> C["USPS · UPS · FedEx · DHL<br/>and your other carrier accounts"]
SS2 --> C
SS1 --> C
ES --> C
style APP fill:#4338ca,stroke:#4338ca,color:#fff
style C fill:#06b6d4,stroke:#06b6d4,color:#fff
One request model in. One rate model out. Each rate remembers which of your accounts produced it, and a purchase goes back through that same account.
| Free, and stays free | MIT licensed. No shipzil account, no proxy, no per-label fee, no paid tier. Commercial use included. |
| No vendor lock-in | Provider details stay at the adapter boundary. Adding a second provider is configuration, not a rewrite. |
| Leave ShipStation without a rewrite | ShipStation v1 and v2 are first-class adapters. Add Shippo or Easyship beside them and move at your own pace. |
| Built for AI agents | Machine-readable docs, AGENTS.md, and per-page Markdown so coding agents get the contract right. |
import shipzil as z
gateway = z.Gateway(shipstation_v2="...", shippo="shippo_test_...")
quote = gateway.get_rates(shipment, carriers={"usps"})
rate = quote.cheapest
label = gateway.buy(shipment, rate)Both providers are queried concurrently. If one fails, the other still returns
rates and the failure lands in quote.errors.
Full runnable example (addresses, parcel, error handling)
import shipzil as z
gateway = z.Gateway(
shipstation_v2="...",
shippo="shippo_test_...",
)
shipment = z.Shipment(
z.Address(
street1="215 Clayton St",
city="San Francisco",
state="CA",
postal_code="94117",
),
z.Address(
street1="1600 Pennsylvania Ave NW",
city="Washington",
state="DC",
postal_code="20500",
),
(
z.Parcel(
weight=z.Weight.of(16, "oz"),
dimensions=z.Dimensions.of(10, 8, 4, "in"),
),
),
)
quote = gateway.get_rates(shipment, carriers={"usps"})
if quote.errors:
log.warning("some sources failed: %s", quote.errors)
rate = quote.cheapest
if rate is None:
# No rates, or rates use mixed/unknown currencies.
raise NoShippingOption(quote.explain())
label = gateway.buy(shipment, rate)Coding agents guess provider semantics badly, so the docs are published in forms
an agent can consume directly. make docs-build produces:
| Surface | Path | Contents |
|---|---|---|
| Index | /llms.txt |
every page, one line each, for retrieval |
| Full corpus | /llms-full.txt |
the complete documentation as one file |
| Per page | /llms.mdx/docs/<page>/content.md |
raw Markdown for a single page |
| Repo guide | AGENTS.md |
scope, invariants and verification rules for agents working in this repo |
Every docs page also has Copy Markdown and Open actions for pasting a single page into a model context.
The invariants an agent most often gets wrong are stated explicitly: a purchase is never retried or redirected, FANOUT rates cannot be bought as one label, and provider service keys are not interchangeable.
| Adapter | Multi-parcel rating | Purchase | Cancel / refund | Verification held in this repo |
|---|---|---|---|---|
| Shippo | per-parcel FANOUT | yes | refund request | rating, test purchase and refund run live |
| Easyship | per-parcel FANOUT | yes | cancellation | captured sandbox responses and payload tests |
| ShipStation v1 | per-parcel FANOUT | yes, base64 label | void | captured rating and testLabel responses |
| ShipStation v2 | native packages[] |
yes | void | rating run live; purchase not run live |
FANOUT rates are sums of separate per-parcel quotes. They are marked
Strategy.FANOUT and cannot be bought as a single label. The Shippo adapter uses
FANOUT even though Shippo supports native multi-piece rating for some carrier and
account combinations.
len(quote) # number of rates
for rate in quote: # rates in configured-source order
quote.errors # source-level failures
quote.excluded # local filtering and provider-reported exclusions
quote.messages # provider warning messages
quote.cheapest # lowest amount, only when currency is known and uniform
quote.fastest # lowest reported delivery_days
quote.explain() # human-readable diagnosticsEvery Rate carries source (your account name), provider, service_key and
currency, which may be None on ShipStation v1.
- No provider health scoring or automatic routing.
fallback=(...)is an order you choose. - Service keys stay provider-scoped. Matching names do not prove equivalent delivery behavior.
- Purchases are never retried or redirected.
AmbiguousPurchaseErrormeans the request may have succeeded; reconcile with the provider first. cheapestreturnsNonefor mixed or unknown currencies. shipzil does not convert money.- Cross-border rating stops when any item lacks weight or value. EEI data is sent only through the Shippo adapter.
See Concepts and Errors for full behavior.
uv add shipzilpip install shipzilNothing else is pulled in. pip list shows only shipzil.
To track unreleased work, install from git:
uv add git+https://github.com/sameerkumar18/shipzil.gitOr work from a clone:
git clone https://github.com/sameerkumar18/shipzil.git
cd shipzil
uv sync
uv run python examples/gateway.pyAlpha. The interface may change before 1.0. See the changelog for what changed in each release.
make check # lint, types and offline tests
make check-compat # Python 3.10 through 3.14, live tests excluded
make test-live # loads credentials from .env and calls real providers
make docs-build # static docs and generated Python referenceQuickstart · Concepts · Providers · International · Errors · Reference
MIT. Commercial use, modification and distribution are allowed under the terms in LICENSE.
"OpenRouter for Shipping" describes the product category. shipzil is not affiliated with OpenRouter.