Skip to main content

Shipping Management

Overview

Ledgerline supports two shipping paths:

  1. Buy a label via EasyPost — automated label purchase + tracking. Available when EASYPOST_API_KEY is configured. Triggered by the AdminJS "Buy label" action or the Discord /buy-label command.
  2. Manual mark-shipped — admins enter carrier + tracking number themselves via the AdminJS dashboard or the Discord /ship command. Always available, regardless of partner config.

When EASYPOST_API_KEY is missing the shipping partner port stays unbound and /buy-label returns a friendly "not configured" message; the manual /ship flow continues to work unchanged.

Configuring shipping types

Shipping types are configured per tenant in the Bot Config admin resource.

Each shipping type has:

FieldDescription
codeStable identifier referenced in shipment records (e.g. STANDARD, EXPRESS)
labelDisplay name shown to customers
costCentsCost in cents (e.g. 599 = $5.99)
estimatedDaysEstimated delivery days

To add a shipping type:

  1. Go to Admin > Configuration > BotConfig
  2. Select your tenant's config
  3. Edit the Shipping Types array
  4. Add one or more entries with code, label, costCents, estimatedDays
  5. Save

Marking an order as shipped

Via AdminJS dashboard

  1. Go to Admin > Commerce > Orders
  2. Find the order to ship
  3. Click the Mark Shipped action button
  4. Enter the carrier name (e.g. USPS, UPS, FedEx) and tracking number in the modal
  5. Click Confirm

Via Discord /ship command

/ship order:<orderId> carrier:<carrier> tracking:<trackingNumber>

Only users with an admin role can run this command.

Idempotency

Marking the same order shipped twice with identical carrier and tracking information is a no-op. The shipment record is not updated a second time.

What happens when an order is marked shipped

  1. Shipment status transitions: pendingin_transit
  2. Order status transitions: PAID / FULFILLINGSHIPPED
  3. shippedAt timestamp is recorded
  4. Events emitted: shipment.shipped, order.shipped — subscribers (customer DMs, dashboard) will act on these in future cards

Buying labels (EasyPost)

When EASYPOST_API_KEY is set, the EasyPost adapter is bound to ShippingPartnerPort and the "Buy label" path becomes available. EasyPost unifies USPS, UPS, and FedEx behind one API.

Via AdminJS dashboard

The Buy Label button appears on the Shipment record row and show page whenever:

  • The shipment status is pending.
  • EASYPOST_API_KEY is configured (the EasyPost adapter is bound).

If the button is not visible, the shipment is not in pending status or EasyPost is not configured.

Steps:

  1. Go to Admin > Commerce > Shipments
  2. Open a shipment in pending status
  3. Click the Buy Label action button
  4. (Optional) enter a service level token in the confirmation dialog (e.g. usps_priority, fedex_ground, ups_ground). Leave blank for cheapest available rate.
  5. Confirm — Ledgerline calls EasyPost, purchases the cheapest matching rate, stores the tracking number + label URL, and transitions the shipment to label_created.

On success the page refreshes and the success notice shows the carrier and tracking number.

Via Discord /buy-label command

/buy-label order:<orderId> service:<token>

Service tokens (carrier_service):

TokenCarrierEasyPost service
usps_priorityUSPSPriority
usps_expressUSPSExpress
usps_groundUSPSGroundAdvantage
fedex_groundFedExFEDEX_GROUND
fedex_2dayFedExFEDEX_2_DAY
fedex_overnightFedExSTANDARD_OVERNIGHT
ups_groundUPSGround
ups_3dayUPS3DaySelect
ups_2dayUPS2ndDayAir
ups_next_dayUPSNextDayAir

Only users with an admin role can run this command.

Idempotency

Calling "Buy label" or /buy-label twice for the same shipment is safe — the second call returns the cached label without re-purchasing through EasyPost. The adapter caches the partner shipment id keyed by the internal shipment id.

Tracking updates

When EASYPOST_WEBHOOK_SECRET is configured and EasyPost webhooks are pointed at https://<your-host>/api/easypost/webhook, Ledgerline applies tracker.updated events automatically:

  • pre_transit → shipment stays label_created
  • in_transit / out_for_delivery / available_for_pickupin_transit, emits shipment.shipped
  • delivereddelivered, emits shipment.delivered
  • failure / cancelled / error / return_to_senderexception, emits shipment.exception

Replays of the same status are no-ops (idempotent).