Shipping Management
Overview
Ledgerline supports two shipping paths:
- Buy a label via EasyPost — automated label purchase + tracking. Available when
EASYPOST_API_KEYis configured. Triggered by the AdminJS "Buy label" action or the Discord/buy-labelcommand. - Manual mark-shipped — admins enter carrier + tracking number themselves via the AdminJS dashboard or the Discord
/shipcommand. 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:
| Field | Description |
|---|---|
code | Stable identifier referenced in shipment records (e.g. STANDARD, EXPRESS) |
label | Display name shown to customers |
costCents | Cost in cents (e.g. 599 = $5.99) |
estimatedDays | Estimated delivery days |
To add a shipping type:
- Go to Admin > Configuration > BotConfig
- Select your tenant's config
- Edit the Shipping Types array
- Add one or more entries with
code,label,costCents,estimatedDays - Save
Marking an order as shipped
Via AdminJS dashboard
- Go to Admin > Commerce > Orders
- Find the order to ship
- Click the Mark Shipped action button
- Enter the carrier name (e.g.
USPS,UPS,FedEx) and tracking number in the modal - 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
- Shipment status transitions:
pending→in_transit - Order status transitions:
PAID/FULFILLING→SHIPPED shippedAttimestamp is recorded- 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_KEYis 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:
- Go to Admin > Commerce > Shipments
- Open a shipment in
pendingstatus - Click the Buy Label action button
- (Optional) enter a service level token in the confirmation dialog (e.g.
usps_priority,fedex_ground,ups_ground). Leave blank for cheapest available rate. - 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):
| Token | Carrier | EasyPost service |
|---|---|---|
usps_priority | USPS | Priority |
usps_express | USPS | Express |
usps_ground | USPS | GroundAdvantage |
fedex_ground | FedEx | FEDEX_GROUND |
fedex_2day | FedEx | FEDEX_2_DAY |
fedex_overnight | FedEx | STANDARD_OVERNIGHT |
ups_ground | UPS | Ground |
ups_3day | UPS | 3DaySelect |
ups_2day | UPS | 2ndDayAir |
ups_next_day | UPS | NextDayAir |
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 stayslabel_createdin_transit/out_for_delivery/available_for_pickup→in_transit, emitsshipment.shippeddelivered→delivered, emitsshipment.deliveredfailure/cancelled/error/return_to_sender→exception, emitsshipment.exception
Replays of the same status are no-ops (idempotent).