Skip to content

Parcel Labels

Packers can get rates, buy and print a carrier label, and book a courier to collect, without leaving Badger: from the fulfilment portal, the Badger POS app and MCP. A label fills in the dispatch tracking, so marking an order dispatched is one tap and the customer's dispatch email carries the tracking as usual. When a return is accepted, Badger can also buy a prepaid return label and email it to the customer.

Parcel labels are off by default (shipping-labels-enabled) and work on every plan. Start with the Sandbox provider: it makes realistic test labels without a carrier account, so the whole flow can be tried before choosing a real provider.

Choosing a provider

Set Settings › Shipping and labels › Label provider (shipping-provider).

Provider Good for Rates Collections Return labels Billing
Sandbox (sandbox) Trying it out, training, dev and test stacks Fixed test prices Confirmed at once Yes Nothing is charged. Labels say TEST LABEL - DO NOT POST and their tracking numbers start BDGRSBX
ShipStation (shipstation) Shops that want to compare carriers (Royal Mail, Evri, DPD, InPost and others) with or without their own contracts Priced, across every carrier on the account Yes, one per carrier Yes The shop's ShipStation balance
Sendcloud (sendcloud) Shops that already use Sendcloud, or want its EU coverage Priced per shipping method No (book them in Sendcloud) Yes The shop's Sendcloud account
Royal Mail Click and Drop (clickanddrop) Shops that only post with Royal Mail or Parcelforce No prices: a list of the shop's services No No The shop's Royal Mail account

The reasoning behind these choices (and why EasyPost, Shippo, Karrio and others weren't picked) is in the carrier research note kept with the implementation plans.

Setting it up

All settings are in Settings › Shipping and labels (group shipping-config). Provider keys are secrets: they're never sent back to the browser (a blank field means "unchanged"), and site transfer (System › Tenants › Move data) never copies them.

Key Default
shipping-labels-enabled false Switches labels on
shipping-provider sandbox sandbox, shipstation, sendcloud or clickanddrop
shipping-label-format pdf pdf for any printer, zpl for Zebra and other thermal printers
shipping-label-size 4x6 4x6, a6, or a4 (the label on a sheet of office paper)
shipping-default-parcel-weight-grams 1000 Used when the products have no weight
shipping-packaging-weight-grams 100 Added to the products' weight
shipping-parcel-presets three Royal Mail sizes Box sizes packers pick from, one per line as Name:LxWxHcm:weightg (the empty box's weight)
clickanddrop-services TPN24:Tracked 24, TPN48:Tracked 48 Click and Drop services, one per line as CODE:Name
shipstation-api-key, shipstation-on-behalf-of ShipStation API key; the partner sub-account (se-…) only for ShipStation partner accounts
sendcloud-public-key, sendcloud-secret-key From a Sendcloud API integration
clickanddrop-api-key From Click and Drop's API settings
shipping-return-labels-enabled false Buy and email a return label when a return is accepted
shipping-returns-location main location Location code returns are addressed to
shipping-return-service cheapest The provider's service code for return labels
tracking-email-out-for-delivery, tracking-email-delivered, tracking-email-problem off, off, on Customer emails as the carrier reports progress; see Parcel Tracking
shipping-webhook-key, sendcloud-webhook-signature-key The secret in the tracking webhook address (made automatically), and Sendcloud's separate webhook key if the integration has one; see Parcel Tracking

Per provider:

  • ShipStation. Create an API key in ShipStation (use a sandbox key while testing) and connect carriers there; Badger rates across every carrier on the account. Labels are bought from the account's balance, so set up auto-funding. A carrier that gives no price over the API (DPD UK, for example) is listed without one.
  • Sendcloud. Create an API integration in Sendcloud (Settings › Integrations) and paste its public and secret keys. Each label is one parcel; a two-box order needs two labels. Sendcloud's own label size applies whatever shipping-label-size says.
  • Click and Drop. Paste the API key from Click and Drop and check clickanddrop-services against the service codes in the account (they vary by account). Click and Drop only returns the label file to Online Business Account customers; for other accounts the order is created and the portal says to print the label from Click and Drop.

Ship-from address. A label's sender is the location the items leave from: the location their stock was allocated at, else the location bound to the fulfiller, else the shop's main location (see Locations). Give it a full address (a first line, a town and a postcode) and a phone number under Shop Settings › Locations; real providers refuse a label without one.

Parcel weight. Give products a weight (in grams) and Badger suggests the parcel weight: the products' weight times their quantities, plus shipping-packaging-weight-grams. Choosing a box preset adds the box's own weight.

In the fulfilment portal

On the Dispatch screen of an order that is posted, a Parcel label panel appears above the tracking form:

  1. Check the parcel: pick a box size or type the dimensions, and check the weight (in kilograms). Providers that can put several parcels on one label offer Add a parcel.
  2. Get rates. The services that can take the parcel, cheapest first, with the price and the delivery estimate where the provider gives one.
  3. Choose one and Buy label. The page shows the label with Print label (a PDF opens in a new tab; ZPL downloads for the printer) and fills in the carrier, tracking number and tracking link below.
  4. Stick the label on and tap Mark dispatched. The label is recorded as sent.

While the parcel hasn't gone, Cancel label voids it with the provider (and refunds it, where the provider refunds). A label can't be bought twice for the same items: cancel the first one to buy another (see Buying safely). If a label's file couldn't be downloaded when it was bought, Print label fetches it again; if the provider still has none, the page says to print it from the provider, with its reference there and the tracking number. Earlier labels, such as cancelled ones, are listed under the panel.

Each fulfiller labels their own items, from the location they send from; a fulfiller only ever sees their own labels.

Courier collections

Courier collections in the portal's account menu (shown when the provider books collections) lists, for each of the user's locations, the labels bought there today that no collection has been booked for. Choose the labels, a day and a time window, and Book collection. A courier collects only its own carrier's parcels, so labels with different carriers are booked as separate collections. Booked collections are listed with their confirmation number and a Cancel button; a label on a booked collection can't be cancelled until the collection is.

An administrator sees every active location that sends parcels; anyone else sees the locations bound to their fulfillers.

In the admin

An order's page lists its labels (and its return labels) in a Parcel labels card: the service, the tracking number (linked to the carrier's tracking page where the provider gives one), the status with the carrier's latest scan, the cost, a link to the label file, Refresh tracking for providers that report it and, while unused, Cancel. Buying happens in the portal, where the parcel is. See Parcel Tracking.

In the POS app

Staff at a store that sends parcels can label web orders from the till. GET /v1/pos/agents/me says whether to offer it (canPrintLabels: labels are on and the user works at a store marked Ship from, or is an admin). The endpoints (tag POS Shipments in the API reference) work on the order's lines sent from the store in the path:

POST /v1/pos/{locationId}/tasks/{orderId}/shipments/rates Rates for the parcels (or the suggested parcel), cheapest first
POST /v1/pos/{locationId}/tasks/{orderId}/shipments Buy a label with a rateId (or serviceCode)
GET /v1/pos/{locationId}/tasks/{orderId}/shipments The order's labels from this store
GET /v1/pos/{locationId}/shipments/{shipmentId}/label The label file (PDF or ZPL)
POST /v1/pos/{locationId}/shipments/{shipmentId}/void Cancel an unused label
GET /v1/pos/{locationId}/shipments/presets The shop's box sizes (shipping-parcel-presets), label format and size, and what the provider can do, so the app needn't hard-code boxes

The store finds the orders to label in its ship tasks (GET /v1/pos/{locationId}/tasks?type=SHIP), each showing its current label. The lines a store labels are those it sends: allocated there, or with no allocation and of the fulfiller bound to it, or at the main location anything not sent from elsewhere (the same rule as a label's sender). Dispatching the task (POST /v1/pos/{locationId}/tasks/{orderId}/dispatch) with no tracking uses the live label's and marks the label sent.

A refusal from the provider is a 422 whose plain-text body says why, in words for staff.

Send an Idempotency-Key header with the buy (a UUID per tap of Buy). A retry with the same key and body, after a lost response, returns the label the first request bought (201) instead of an error; a retry while the first is still buying is a 409; the same key with a different body is a 422.

Buying safely

A label costs money, so buying is built never to buy twice or lose one that was bought:

  • One buy at a time. Before calling the provider, a buy claims the lines: a shipments row in DRAFT holding a slot (site, order and fulfiller; or site and return for a return label), protected by a unique sparse index (shipment_slot_idx, created by the worker). A second buy for the same lines, from another tab, a retried request, MCP or another server, is refused with "already being bought" and never reaches the provider. The slot stays held by the bought label while it is unused (any live return label, for a return), and is freed when the label is cancelled or sent. If the provider refuses, the claim is removed. A claim left by a server that died mid-buy is taken over after five minutes (logged with its id, as it may have been bought at the provider). The service also reads the slot's holders back after claiming, so the oldest claim still wins if the index hasn't been built yet.
  • A bought label is always recorded. Once the provider has sold the label, a failure to download its file (a broken link, an expired token, a file that isn't valid) never fails the buy: the label is recorded without a file. Printing it later (portal, admin, POS) fetches the file again from the provider (ShipStation GET /v1/labels/{id}, Sendcloud's documents endpoint, Click and Drop's label endpoint) and keeps it. If the provider still has none, staff are told to print it from the provider, with the provider's reference and the tracking number. If the label can't be saved in Badger at all, it is cancelled with the provider where possible, and staff are told either way. The provider's ids are logged so it can be found, but no addresses are.
  • Keys stay with the provider. Provider calls never follow redirects, and responses are read up to 8 MB. ShipStation's label download links are fetched only over https from shipengine.com or shipstation.com hosts, and without the API key (the links carry their own token). Sendcloud and Click and Drop files come from their fixed API hosts.

Through MCP

shipments (mcp:read) lists an order's labels with their tracking, or a location's labels awaiting collection and its booked collections. manageShipments (mcp:write) quotes, buys and voids labels, books and cancels collections, sends return labels (returnLabel with the return's id) and refreshes a label's tracking (refreshTracking). Buying spends the shop's money, so the tool is marked open-world. After buying, dispatch the order with manageFulfilment using the returned tracking.

Return labels

With shipping-return-labels-enabled on and a provider that makes return labels, approving a return in the admin also buys a return label, from the customer's delivery address to the returns location, and emails it to the customer. A return that was accepted another way (recorded by staff, or accepted automatically) gets one from Send a return label on the return's page, or manageShipments returnLabel. Sending again re-emails the same label rather than buying another. The shop pays for return labels.

The email (email/returnLabel, HTML and text) can't attach files, so it carries a download link that works for 30 days without signing in, along with the drop-off steps, the shop's return instructions (customer-return-instructions), the service and the tracking number. The link is signed with a per-site secret (return-label-link-signing-key, made once on first use: servers racing to make it all keep the one stored first) and names only the label; an altered, expired or cancelled link, or one for a return that has been rejected or finished, shows a plain "expired" page. If buying the label fails when a return is approved, the approval still stands and the admin says so.

Rejecting a return cancels its unused return label with the provider, so the shop isn't charged for a parcel that shouldn't come back. The return's history says whether each label was cancelled; one that couldn't be is noted there to cancel with the provider by hand, and the rejection stands either way. A label already in the post is left alone.

Logs about labels carry the shipment, order, return and site ids and the provider's references, never the provider's error wording (which can repeat an address); that wording is only shown to staff (and, through MCP, to the model).

Data

Collection
shipments One label: the order (and return), fulfiller, location, lines, provider ids, carrier, service, tracking, cost, parcels, the sender and receiver addresses as they were, and its status (LABEL_PURCHASED, VOIDED, DISPATCHED, IN_TRANSIT, DELIVERED, EXCEPTION; DRAFT only while a buy is in progress, never listed), plus the slot described under Buying safely and the carrier's tracking (see Parcel Tracking)
shipment_labels The label file, under the shipment's id (provider download links expire, so Badger keeps its own copy; a missing one is fetched again when printed)
shipping_pickups A booked courier collection: location, day, window, confirmation, labels

Everything is keyed by site. Only ShippingLabelServiceImpl reads and writes these (tracking through TrackingService, which saves via ShippingLabelService); the portal, admin, POS API and MCP all go through ShippingLabelService (POS through PosShipmentService, return emails through ReturnLabelService). Each provider is a ShippingProvider bean; adding one is a new bean and a new value for shipping-provider. Logs carry shipment, order and site ids, never addresses.

Limitations

  • Provider endpoints are not yet proven against live accounts. Each provider is unit-tested against canned responses taken from its public documentation. The details not confirmed against a live account are listed in each provider class's javadoc (for ShipStation: A6/A4 label layouts, the label's tracking_url and the pickup request body; for Sendcloud: the sender (from_*) fields, the currency of method prices, return parcels and tracking status ids). Try a sandbox account before a shop relies on one.
  • Tracking (webhooks, polling, customer timeline and emails) is described in Parcel Tracking; Click and Drop labels aren't tracked.
  • No customs forms (CN22/CN23), so labels are for domestic parcels. No service points, lockers or drop-off-point search.
  • Sendcloud and Click and Drop can't book collections here; Click and Drop can't make return labels.
  • A return accepted automatically (customer returns with customer-returns-require-approval off) doesn't get a label until someone sends one from the return.
  • Labels become Sent when the order is dispatched in the portal or from a POS ship task; dispatching elsewhere (admin, MCP) is noticed when someone tries to cancel the label.