justinholtweb/craft-shipper

ShipStation integration for Craft Commerce — export orders, receive tracking, quote live carrier rates, and see every request in a log.

Maintainers

Package info

github.com/justinholtweb/craft-shipper

Type:craft-plugin

pkg:composer/justinholtweb/craft-shipper

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-08-29 14:23 UTC

This package is auto-updated.

Last update: 2026-08-29 14:24:34 UTC


README

ShipStation integration for Craft Commerce 5. Orders go out, tracking comes back, and every request either end makes is on a screen you can read.

Shipper connects Commerce to a ShipStation Custom Store: ShipStation polls an endpoint on your site for orders, and posts back a shipment notification when a label is bought. Shipper stores those shipments as its own data — there is no field to build, no Matrix to configure, no handles to type into settings. Install it, paste a URL into ShipStation, done.

Requirements

  • Craft CMS 5.3+
  • Craft Commerce 5.0+
  • PHP 8.2+

Installation

composer require justinholtweb/craft-shipper
php craft plugin/install shipper

Then open Settings → Plugins → Shipper, set a username and password, and copy the URL to Custom XML Page into ShipStation's custom store setup.

Editions

Lite (free) Pro
Custom Store export + shipment notifications
Shipments stored as data, with a CP index
Tracking links for 20+ carriers
Panel on Commerce's order screen
craft.shipper.* Twig API
Shipped-status mapping
Console commands
Connection log with request/response payloads
Partial shipments — per-item counting
Two-way status mapping
Custom field object templates
Sync now — force a ShipStation re-import
Live carrier rates at checkout

How it connects

ShipStation drives the conversation. It calls one endpoint with an action:

  • ?action=export&start_date=&end_date=&page= — Shipper answers with the orders XML.
  • ?action=shipnotify&order_number=&carrier=&service=&tracking_number= — Shipper records the shipment and moves the order on.

Authentication is HTTP Basic, which is what ShipStation's store settings offer. Apache commonly strips the Authorization header, so Shipper also accepts an auth_key query parameter — append it to the URL and the connection works regardless of what the server does with headers.

What ShipStation is told

Order number and ID, dates, status, payment method, shipping method, currency, totals, tax, shipping cost, customer notes, internal notes, custom fields, the customer with bill-to and ship-to addresses, and every shippable line item with SKU, name, image, weight, quantity, unit price and its options. Cart-level discounts go across as an adjustment line so the totals reconcile. A single unit of a single product also carries its dimensions.

Non-shippable purchasables are left out, and an order with nothing shippable is skipped rather than sent and rejected.

Partial shipments

ShipStation sends the items in each shipment. Pro counts them, and only completes the order once every shippable unit has gone. Until then the order can sit in a "partly shipped" status of your choosing.

Retries are handled properly: ShipStation re-sends a notification hourly and includes no notification ID, so Shipper keys a shipment on its tracking number plus carrier. A retry — even one that differs only in carrier casing — is recognised and ignored rather than counted twice.

Live rates (Pro)

With a ShipStation API key, Shipper offers real carrier rates as Commerce shipping methods. Quotes are cached against a signature of the cart, so re-costing the same cart is free and changing it re-quotes.

If ShipStation is slow or unreachable, no live methods appear and checkout carries on with whatever shipping methods your store defines. A carrier outage can never stop a customer paying.

Twig

{% for shipment in craft.shipper.shipments(order) %}
    {{ shipment.getLabel() }}
    {% set url = craft.shipper.trackingUrl(shipment.carrier, shipment.trackingNumber) %}
    {% if url %}<a href="{{ url }}">{{ shipment.trackingNumber }}</a>{% endif %}
{% endfor %}

{% set progress = craft.shipper.progress(order) %}
{{ progress.shipped }} of {{ progress.total }} items shipped

craft.shipper.latestShipment(order), craft.shipper.isShipped(order) and craft.shipper.knownCarriers() are available too.

Console

php craft shipper/export/preview            # the exact XML ShipStation would be served
php craft shipper/export/preview --order=1042
php craft shipper/export/count              # how many orders match your settings
php craft shipper/export/list
php craft shipper/rates/quote --verbose     # quote a cart through the checkout code path
php craft shipper/sync/refresh              # ask ShipStation to re-import now
php craft shipper/sync/test
php craft shipper/sync/carriers
php craft shipper/log/list
php craft shipper/log/prune                 # point cron at this

shipper/export/preview runs the same builder the endpoint does, so a payload that looks right there cannot differ in production.

Debugging a quiet store

If ShipStation says it imported nothing, open Shipper → Log. Every request it made is there with its status code, timing, and — on Pro — the full payload both ways. A 401 means the credentials do not match; an empty export means your status filter excludes everything, which How many orders match? on the settings screen will confirm in one click.

License

Proprietary. See LICENSE.md.