Shipping labels are one of those features that look trivial until you actually build them. You need a carrier-rating API, a payment step, PCI scope for the card data, and — the part nobody warns you about — a fraud problem, because labels bought with stolen cards are a top chargeback category. Real goods, shipped fast, disputed weeks later.
We open-sourced Lifted ShipKit to collapse that whole stack into a few lines of code. It's MIT-licensed, self-hostable, and it ships with a drop-in checkout widget that forces 3-D Secure on every card charge.
Repo: https://github.com/LiftedHoldings/lifted-shipkit
What it actually is
ShipKit is a Kotlin/JVM backend plus a dependency-free browser widget. The backend wraps EasyPost for real multi-carrier shipping — address verification, live USPS/UPS/FedEx rate compare, SmartRates, label purchase, batch, scan forms, customs, and tracking webhooks — and adds a payment layer on top of it. The widget is a UMD global (window.ShipKit), so it mounts in plain HTML, React, Vue, Svelte, or anything else with a <script> tag and no build step.
The part I want to talk about is the payment layer, because that's the design decision that makes ShipKit different from "just call EasyPost yourself."
Why forced 3-D Secure is the whole point
Most teams that add shipping wire a rating API straight to a payment processor and then quietly own two things they didn't sign up for: the PCI scope for card data, and the chargeback liability when a fraudster buys a label with a stolen card.
ShipKit forces 3-D Secure on every card charge. The issuer authenticates the buyer before the label prints, which shifts fraud-and-chargeback liability off the merchant. Card data goes through hosted fields, so it stays out of your PCI scope. This isn't an enterprise upsell or a config flag you can forget to turn on — it's always on, by construction.
For a product whose fraud profile is "real goods, shipped immediately, disputed later," making authentication non-optional is the correct default.
60-second try
No account, no keys — the published Docker image comes up immediately (shipping and payment features return 503 until you supply an EasyPost key and 3DS credentials):
docker run --rm -e SHIPKIT_PORT=8080 -p 8080:8080 ghcr.io/liftedholdings/lifted-shipkit
Then open http://localhost:8080.
Drop it into your own checkout
The headline use case is keeping your app and your checkout and adding ShipKit for the rate-compare -> pay -> print-label step. Mount a node, point it at your backend, wire a callback:
<div id="ship"></div>
<script src="/js/shipkit.js"></script>
<script>
ShipKit.init({
mount: '#ship',
endpoint: '/api',
apiKey: 'pk_live_your_publishable_key', // publishable pk_… key — safe in the browser
onPurchase: ({ trackingCode, labelUrl }) => {
// hand the finished label back to your order flow
}
});
</script>
Keys come in two scopes: the browser widget uses a publishable pk_… key (the backend confines it to the customer flow), while your server calls use a secret sk_… key that never appears in client code. Every surface restyles with --sk-* CSS variables — no !important, no build step.
Self-host it fully
git clone https://github.com/LiftedHoldings/lifted-shipkit.git
cd shipkit
cp .env.example .env # EASYPOST_API_KEY + your 3DS keys
./gradlew build # Kotlin 2.0.21, JVM 17
./gradlew shipkitKeygen -Plabel=my-store # mint an API key
./gradlew run # API + widget on :8080
Every setting is read from the environment. Two optional, env-driven hooks let a platform operate a fleet of instances from a control plane — a bearer-guarded POST /api/config/markup to update the shipping markup remotely, and a fire-and-forget label.purchased webhook (carrier cost and buyer charge in integer cents) — both off by default when their tokens are unset.
Shipping as a profit center
One design choice worth calling out for anyone building a store: whoever owns the payments account owns the markup. ShipKit prices every label at the carrier rate plus your own percentage and fixed fee (percentage_markup + fixed_fee_cents), applied server-side and shown at checkout. Self-host it and that margin is 100% yours on every label.
Why open source it
Because the honest version of this tool is one you can read, fork, and run yourself. MIT license, dependency-free frontend, clean modular backend, no per-label SaaS fee when you self-host. If you'd rather not run infra, there's a managed tier and a 3-D Secure merchant-account tier — but the code, the widget, and self-hosting stay free.
- Live demo: https://liftedholdings.com/shippingtool
- Repo + docs: https://github.com/LiftedHoldings/lifted-shipkit
- The payments side (interchange-plus merchant services with 3-D Secure, built by the same team): https://liftedpayments.com
If you build something with it, or you want ShipKit woven into your own framework, we'd genuinely like to hear about it. PRs and issues welcome.
Top comments (0)