DEV Community

Daniel Wilson Kemp
Daniel Wilson Kemp

Posted on

Lifted ShipKit: open-source multi-carrier shipping labels with forced 3-D Secure

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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)