User guide

Everything Shipnest does, top to bottom.

Each section maps to a real screen — the path in /like-this opens the page directly when you're signed in. Use the sidebar to jump, or scroll for the full tour.

Get started

Quick start

/onboarding
  1. Sign up at /signup. Free tier covers 100 labels/month with no credit card.
  2. On /onboarding, the 4-step wizard walks you through naming your warehouse, picking a default currency, connecting your first sales channel, and connecting at least one carrier.
  3. Place a test order on the connected channel. It syncs into /orders automatically (within 10 min via cron, or immediately via webhook for Shopify / Woo / BigCommerce).
  4. Open the order detail and click Rate-shop. Quotes from every enabled carrier appear sorted by price. Pick one and click Buy label.
  5. Print the label (browser print or via PrintNode if wired), drop the parcel with the carrier, and the handover scan flips the row to "handed over".

That's the core loop. Everything else below makes the loop faster, cheaper, more automated, or more compliant.

Tip
Use the sidebar to skip ahead. Each section's monospace pill (e.g. /orders) is the in-app route — open the dashboard in another tab to follow along live.

Sample-data mode on fresh workspaces. Every list page (Orders, Shipments, Customers, Products…) renders a small set of demo rows until you have real data — so you can explore the UI from minute one without staring at empty tables. Each demo-mode page shows a brand-tinted banner at the top spelling out exactly what would bring real data in ("Real orders land here once you connect a channel — or create one manually") plus one-click CTAs to do it. The badge in the topbar reads Demo in that state, and flips to Live the moment the first real row is persisted.

Fresh-tenant gates. Pages that depend on operator setup (/labels/new, /carriers) don't render a half-broken form when you're missing prerequisites — they show a friendly card with direct CTAs ("Connect a carrier", "Add a warehouse") so the next click is obvious.

Dashboard

/dashboard

/dashboard is the start-of-day glance. Six KPI cards across the top:

  • Orders awaiting ship — pending count, deep-links to /orders filtered to AWAITING_SHIPMENT + ON_HOLD.
  • Labels printed (7d) — sparkline + week-over-week delta.
  • Pending handover — labels printed but not yet physically handed to the carrier (orange when > 0).
  • At SLA risk — orders whose channel-derived ship-by deadline is today or past (red when > 0).
  • Spend (7d) — per-currency stack: a multi-shop org running US + UK + EU sees $2,000 · £900 · €800 rather than a fake FX-converted single number.
  • Open claims — insurance claims you filed inDRAFT / SUBMITTED / UNDER_REVIEW.

Below: a Ready to ship strip with the 5 most recent unshipped orders (one-click into the order detail), a Carrier split chart showing volume + cost per carrier, and a Channel prompt that lights up when you have channels available to connect.

The dashboard polls every 30 seconds while the tab is focused so the numbers stay fresh without a hard refresh.

Daily workflow

Orders

/orders

/orders is the table of every order across every channel — synced from Shopify / Amazon / Etsy / eBay / Woo / BigCommerce / Squarespace / Magento, plus manual orders entered via /orders/new.

The DataTable supports global search, multi-column sort (shift-click to add a sort), per-column filters (text / number / select / multi-select / boolean / date), column reordering, hiding, pinning, density toggle, group- by, saved views and CSV export. Every preference is per- user per-table, synced across devices via the TablePreference table.

Channel column reads the denormalised sourceName field first (so the channel label survives a store disconnect), falling back to the live Store relation, finally to "Manual".

Ship-by column shows the channel-derived deadline (Amazon 1 business day, Shopify 2 days, Etsy 3 days) with colour coding: red for overdue, amber for today, soft amber for next 2 days, grey for later.

Bulk actions when you select multiple rows: add tag, remove tag, change status, queue to /batch for fan-out rate-shop + buy.

Status badges. An order in AWAITING_SHIPMENT with at least one non-voided shipment that doesn't yet cover every unit renders as a yellow "Partially shipped" badge. The underlying status is still AWAITING_SHIPMENT (rules, cron sync and plan caps treat it the same), but the badge + matching multi-select filter give you a one- click slice of in-flight partials.

Order detail (/orders/[id]) renders the full picture: items + images with a per-line "Shipped X / Y" column, ship-to / ship-from / bill-to addresses, channel + customer, applied automation hints, customs declaration form (international lanes only), hazmat panel, rate-shop panel, a Shipments card listing every non-voided shipment with its line items, and a terminal-status banner once the order ships.

Tags. The Add-tag picker on the order detail surfaces every tag the org has ever used — including the ones that came in via channel sync (Shopify, Etsy seasonal labels, Woo product categories) which never seeded the local catalogue. Picking from existing tags keeps a single canonical name across the shop; creating a new tag opens an inline 24-swatch colour picker so the pill renders in the right tone on first paint.

Shipments

/shipments

/shipments lists every label you've bought, voided or had returned. Same DataTable surface as orders: search, filter, sort, save views, export CSV.

Per-row columns: tracking number, carrier, service, status (label-purchased / in-transit / out-for-delivery / delivered / exception / returned / voided), cost in the shipment's native currency, CO₂e estimate (hidden by default; reveal via column chooser), handover state.

Shipment label is operator-readable. The Shipment column + the detail-page title show the order link when there is one (#1042), a truncated tracking number for Quick Labels (Quick label · 871636…328665), and fall back to a short internal suffix only when neither exists. The table search operates on the rendered label too — typing 1042 finds the matching row instead of searching by an internal id you can't see.

Inline copy on hot cells. Tracking numbers (shipments), order numbers (orders), customer emails (customers) all carry a one-click copy button beside the value — saves the select+cmd+c dance every time you paste one into a support ticket. Success swaps to a green checkmark for a moment so you know it landed.

CSV export. The Download CSV link on the toolbar pulls the current org's shipments (most-recent 5000) as a flat CSV — created / shipped / delivered timestamps, carrier, service, tracking, order number, ship-to address, weight, cost, currency, handover stamp. Hand it to finance for end-of-month reconciliation or to procurement for carrier-spend analysis.

Shipment detail covers print history, claims filed against the shipment, third-party insurance policy if any, tracking timeline, void button (with carrier-specific window: UPS 90d, FedEx 60d, DHL 28d, USPS 28d, Royal Mail 14d, EasyPost 30d).

Voiding a label cancels the channel fulfillment too. When you void, Shipnest tells the source channel so the merchant's order view stops showing a now-dead tracking number. Native partial cancel works on Shopify, WooCommerce, BigCommerce, Squarespace and Magento — Etsy, eBay and Amazon don't expose a cancel API and you un-ship from the channel admin yourself. The shipment detail surfaces a colour-coded banner showing the channel state (canceled / needs retry / API doesn't support it / failed) plus an Actions → Retry cancel on <channel> menu item for pre-feature voids and transient channel failures.

Proof of Delivery

On any DELIVERED shipment, the Documents → Fetch POD from carrier menu item pulls the signed POD PDF straight from UPS, FedEx or DHL and persists it on the shipment row so future clicks open the cached copy instantly. Useful for chargeback disputes + "I never got my package" tickets — no more screenshotting the carrier portal. USPS, Royal Mail and EasyPost don't expose POD via API; for those you still go through the carrier dashboard.

Hold at carrier pickup point

Toggle Hold at carrier pickup point on the rate-shop panel (or at /labels/new) and the parcel routes to the nearest UPS Access Point, FedEx Office or DHL ServicePoint instead of the recipient's door. EasyPost forwards the flag to whichever downstream carrier ran the lane. The carrier picks the location based on the destination postal code + service eligibility — no location-selector UI in v1, auto-pick covers most B2C use. Surcharge appears on the rate quote before you buy. The public tracking page calls GET /api/locations/:carrier?postalCode=… to show the buyer the 5 nearest Access Points / FedEx Office counters / DHL ServicePoints to their address.

Carrier-refined ETA

Every tracking refresh pulls the carrier's latest estimated-delivery date — UPS SDD, FedEx ESTIMATED_DELIVERY, DHL estimatedDeliveryDate. When it changes, the shipment detail shows Carrier ETA refined to Tue Apr 14 · updated 12m ago under the status line. USPS / Royal Mail / EasyPost don't refine ETAs via API; their shipments stick with the buy-time estimate.

Tracking updates land in two ways depending on the carrier:

  • Push (real-time) — EasyPost and FedEx. We auto-subscribe their webhook at carrier connect; the receiver verifies the signature and writes a TrackingEvent in seconds.
  • Polling (30-min cadence) — UPS, DHL Express, USPS, Royal Mail. Their APIs are file-based or don't expose webhook subscription; the/api/cron/tracking cron fetches every in-transit shipment every 30 minutes as the fallback.

Sometimes you need to ship a one-off package that didn't come from a sales channel — a return to a supplier, a between-warehouse transfer, a sample to a prospect, an Etsy order the customer placed off-platform. /labels/new (also reachable from the topbar + Create menu) builds a label end-to-end without touching /orders.

Three modes for ship-from

  • Warehouse — pick any of your saved warehouses. The default for the common case.
  • Saved — pull an entry from the address book (/customers?tab=addresses) when you ship from a co-packer or a 3PL that isn't a real warehouse.
  • New — paste a fresh address inline with a Save to address book toggle so a one-off pickup location becomes reusable next time.

Same three modes for ship-to

Pick a customer from the address book, paste a brand-new address, or even ship to one of your own warehouses (the transfer-between-locations case). Every flow respects the live address validator before the rate-shop fans out. Three engines back the check: a local format pass (country-specific postal patterns) always runs; Smarty or USPS WebTools env-wired by the operator for street-level DPV lookups; otherwise we fall back to whichever connected carrier on this org has an address-validate endpoint (UPS XAV, FedEx/address/v1/addresses/resolve, USPS/addresses/v3/address). So a tenant on a deploy with no Smarty quota still gets live verification at zero marginal cost.

Then it's normal rate-shop + buy

Parcel + value (with multi-piece support), customs if international, the same RateShopPanel you use on order detail. The shipment lands in /shipments with a brand-tinted "Quick label" badge instead of an order number, so it's never confused with a channel order in the list.

Quick labels still respect plan caps
Standalone shipments count against your monthly label cap — it's the same buy-label code path. Volume tracking, audit logs and webhooks all fire normally. The one thing missing is partial fulfillment / split orders (no order to carve from).

Two flows for orders that won't ship as a single parcel. Pick the one that matches reality:

  • Partial fulfillment — same address, ship in multiple parcels over time. Order keeps its identity, channel link and order number; flips to SHIPPED only when every unit is covered. The right default for "backorder one item, ship what's in stock now."
  • Split order — different addresses for parts of the order. Carves units off the parent into NEW sibling orders ( <orderNumber>-1, -2, …). The original stays intact; the operator changes ship-to per sibling. Available under Actions → Split order on the order detail.

Partial fulfillment

The rate-shop panel on /orders/[id] renders an "Items in this shipment" picker listing every line with units left to ship. Each line defaults to its remaining qty (i.e. one click ships everything left); reduce a qty to leave units pending for a follow-up label. The Buy button stays disabled until at least one unit is selected.

Per-channel hint above the picker:

  • Shopify, BigCommerce, eBay, Amazon, Magento — each shipment creates its own partial fulfillment on the channel. Buyer sees the order as partially fulfilled with this batch's tracking attached only to the items that shipped.
  • Etsy, Squarespace — every partial adds another tracking entry on the channel. Per-line granularity isn't exposed by their APIs; the buyer sees N tracking codes accumulating on the order.
  • WooCommerce — the order stays in processing across partials with each shipment's tracking landing as a customer- visible note + Shipment-Tracking meta keys. The status flips to completed only when the final partial ships.

The order detail shows the breakdown live: every line carries a "Shipped X / Y" column (green when fully shipped, amber while partial, grey on lines with nothing out yet) and a Shipments card lists every non-voided shipment with the items it covered. Voiding any shipment puts those units back in stock and re-opens the order if it had been fully covered before.

Splitting orders

The Split dialog renders a table with one row per line item and one column per destination order. The first column — "This order" — is the original parent and shows the live remainder; new columns are siblings to be created. Type how many units go to each new order; whatever you don't move stays on the parent. The original is never cancelled just because units left.

The dialog handles two real-world cases: line items with the same SKU on different OrderItem rows (Shopify imports two cart lines as separate items) and splitting a qty=2 line so 1 unit stays on the parent and 1 unit goes to a new order.

/batch isn't a list with three buttons — it's a full workspace where you fix every per-order shipping field BEFORE pulling the trigger and run the whole post-buy flow (pickup, manifest, packing slips) one click away. Built so a 50-order morning fulfilment block runs end-to-end without ever opening an individual order.

Pre-flight blockers panel

An amber panel above the table summarises every row that's not ready: missing weight, incomplete ship-to, missing customs HS code or origin, hazmat undeclared, no carriers connected, no warehouse resolved, or a DDP-on-platform conflict (DDP shipping with only a platform carrier connected would bill duties to the operator). Rows with hard-error blockers are skipped automatically on Quote-all so you don't burn carrier rate-limit on requests that would fail.

Smart filter chips

Click a chip to narrow the batch: International, Domestic, EU, US, Heavy (>5kg), Hazmat, Has blockers, Pending, Quoted, Bought. Multi-select with AND semantics. Composes with the DataTable's own search + column filters — chip to "EU", then filter status=Quoted, then bulk-edit the result.

Edit anything inline

Every row exposes inline editors for: weight, warehouse, signature level, insurance, incoterms, hazmat, carrier + service pin. Click the destination cell → ship-to address dialog. Click the products cell → line-item dialog with editable qty, per-unit weight, and on international rows per-line HS code / origin / declared value. Click the parcels cell → multi-parcel dialog with dims, weight, packaging and declared value per box. Every edit is optimistic-local first, server-sync after — failed saves toast the reason without rolling back the rendering.

Bulk edit N rows in one shot

Select rows + click Bulk edit → tri-state dialog with 22 fields: weight, warehouse, parcel dims, apply package preset, signature, insurance, incoterms (with DDP auto-disabled when only platform carriers are connected), bill-duties-to, customs (HS, origin, contents type, paperless, signer, non-delivery), Saturday, hold-at-location, customer notify, hazmat, tag add/remove, priority, assignee, hold/unhold, append note. Tick the fields you want to apply — unchecked fields pass through untouched. Apply runs in parallel with per-row try/catch; toast reports affected count + failures.

Quote → pick override → Buy → print → pickup → manifest

Quote all fan-outs rate-shop across selected rows (skipping any with hard-error blockers). Per-row Compare popover on the Cost cell shows every quote with auto-tagged Cheapest + Fastest badges — pick a different one to override for that row. Or pin a specific carrier+service permanently from the Pin carrier dropdown (writes the hint so the same carrier wins next time too).

Buy all charges every label in parallel + saves the lot to /shipments. Post-buy a banner appears: "Just bought 12 labels — schedule pickups?" → opens a dialog grouped by carrier+warehouse, you pick one shared date+window, every pickup books with the real carrier API. Manifest in the toolbar creates+closes one manifest per carrier group across the bought rows — SCAN form PDF lands on /manifests for the driver. Slips sends packing slips to PrintNode (or opens browser tabs with 150ms gaps so your pop-up blocker doesn't swallow the batch).

Void · cancel · refund without leaving

Per-row void chip on bought rows + bulk Void button quote the potential carrier refund + reminds you about per-carrier processing fees + void-window caps. Cancel bulk-cancels orders + pushes the cancel to the source channel (Shopify orders/cancel, Woo status=cancelled). Refund bulk-records refund intent (amount + reason) on /returns + /reports for tracking — finalise the payment-side money refund in your payment provider or channel admin where the gateway transaction id is visible.

Save bulk edits as a rule

After applying a bulk patch, a brand-tinted banner offers Save as rule. Click → dialog lets you name the rule + pick ONE basic lane condition (domestic / international / eu-export / eu-import / intra-eu) + reviews the auto-translated action list. Save → creates an AutoRule disabled so you review + enable on /rules/[id]. Bulk work today becomes automation tomorrow.

Multi-currency money

Stats at the top stack money per currency (a batch with UK + US orders shows Quoted total: $4,500 · £1,200 rather than a meaningless mixed sum). The Money primitive renders the largest currency as the hero with smaller currencies underneath.

/scan is one barcode-first console with three modes — Print label, Pick & pack, Handover — selectable from the segmented control at the top, also keyboard-driven via 1 / 2 / 3. Each mode is a focused workflow; the URL keeps the active mode (/scan?mode=pick, etc.) so you can deep-link floor-station bookmarks.

All modes share the same hardware ergonomics: auto-focus + window-focus re-focus so a Zebra / Honeywell scanner with Enter-terminator works straight out of the box, and the same WebAudio beep palette (880Hz OK / 520Hz warn / 180Hz error) so a worker moving between modes hears consistent feedback.

Mode 1 · Print label

Scan an order barcode → Shipnest loads the order, runs rate-shop with your configured strategy, picks the winner, buys the label, dispatches to your default printer via PrintNode. The session feed shows the last scans with status + tracking + carrier. Toggle off Ship-on-scan if you want the scan to just match the order without auto-buying.

Mode 2 · Pick & pack

Two-stage workflow that sits between order receipt and label buy:

  1. Scan the order barcode (or type the order number). System loads the order and renders a checklist of SKUs with quantities.
  2. Scan each SKU off the shelf. Each scan ticks the matching line's counter. Mismatches beep error and don't increment.
  3. When every line is fully scanned, hit Mark packed & continue. A pack.verified audit row writes and you redirect to the order detail for rate-shop + buy.

Pack verification is informational — nothing blocks the label buy if you skipped the scan. It exists to catch wrong-item-in-box errors before they become refunds / chargebacks, and the audit row tells ops who packed what at what time when a customer complaint arrives.

Mode 3 · Handover

Records the moment a parcel physically leaves the warehouse. Orthogonal to the carrier-driven shipment status — a label can show IN_TRANSIT per the carrier without ever being scanned handover, and vice versa.

Today's scans show in the Scanned today feed (org-wide, persisted via AuditLog so it survives refresh / device swap / laptop lid close). Each row shows tracking + carrier + order number + operator name so two workers sharing a station can tell their scans apart. Use /scan/handover/history for the full audit trail with operator + outcome filter + date-range search + CSV export.

Rate calculator

/rate-calculator

/rate-calculator is an ad-hoc rate quote without an order. Paste any addresses + parcels + options → see quotes from every enabled carrier account. Useful for customer service answering "how much would shipping be for X?" or comparing your UPS rate vs your contract rate-card before committing.

When an integration is genuinely retired — a Shopify store you closed, an Etsy shop you no longer sell on, a Magento install you migrated off — disconnect it from /integrations and you can clean up the orphan rows it left behind.

Two-step cleanup

  1. Disconnect the channel. While a store is still connected, delete is intentionally blocked. A live integration could re-sync a deleted order on the next cron tick or webhook, and we'd rather make you remove the link first than let a stale tab quietly resurrect what you nuked.
  2. Bulk-delete from /orders and /shipments. The Delete button only appears on rows that qualify (manual orders, or orders whose channel was disconnected; voided / draft shipments only). Live shipments block the order delete — void the label first.

What gets cascaded

  • Order delete sweeps voided shipments, line items, customs blob, parcel state. Tracking events + insurance policies on voided shipments cascade at the schema level.
  • Shipment delete sweeps tracking events, pieces, returns, insurance policy and terminal claims (DRAFT / DENIED / CLOSED). Active claims (SUBMITTED / UNDER_REVIEW / APPROVED / PAID) block — resolve them first.

Every delete writes an audit row (order.deleted or shipment.deleted) carrying the order number / tracking number / source channel so you can see who deleted what from /audit.

There is no undo
Delete is permanent — by design. The bulk-delete affordance only counts rows that actually qualify, so accidentally selecting a connected-store row and clicking Delete simply skips it. But once a row is gone, it's gone.
Automations

/rules is where you encode operational policy: which carrier handles which lane, when to add insurance, when to flag for review, what tags to apply. Rules fire automatically on five triggers and stack cleanly so a single order can be touched by many.

The /rules/[id] builder is a visual nested AND/OR tree. Add a rule, choose a trigger, build the condition tree, list the actions. Click Test against an order to dry-run before saving.

Don't love the visual rule builder? The Describe with AI button at the top of any rule editor opens a small dialog where you type what you want in plain English:

  • "Tag every Shopify order over $100 as VIP"
  • "Add insurance for orders over $200 going outside the US"
  • "Hold orders missing a phone number"
  • "Route GB customers to DHL Express with signature required"

GPT-4o produces a strict rule shape (name, description, trigger, conditions, actions). Every field gets validated against the rule engine's registry — if the model invents an action that doesn't exist we drop it and flag a warning instead of failing the whole response.

The result pre-fills the editor. We don't persist anything until you click Save yourself — review the conditions, tweak any param, run Test against a real order, then save. The Regenerate button lets you iterate on the prompt without retyping.

It overwrites the form, not merges
Apply replaces every field on the editor with the AI's output. That's the right default for "compose for me" — if you want to combine an AI suggestion with an existing rule, generate first, copy the bit you want, then re-open the existing rule and paste.

The top of /rules carries a "Suggested automations" card that mines your last 30 days of activity and surfaces patterns you keep doing manually — "you picked UPS for 8 of 8 orders shipped to GB", "12 of 14 Shopify orders carry the tag wholesale". One click creates the rule.

What we look for today

  • Carrier by destination country — when ≥80% of shipments to a country use the same carrier across at least 5 events, we suggest destination.country eq X → SET_CARRIER Y.
  • Tag by channel — when ≥80% of orders from a channel carry the same tag, we suggest storeKind eq X → ADD_TAG Y.

More detectors will land over time. Adding a new pattern to the engine is a single function append on our side — no model retraining or per-tenant config.

How Apply works

  • Created rules land disabled so the operator reviews the condition + action before any order touches them.
  • The minimum bar is 5 events at 80% confidence — low enough to spot real patterns, high enough that the card stays useful instead of noisy. We cap at 5 suggestions per visit.
  • Suggestions whose pattern is already covered by an existing rule get filtered out automatically, so the same suggestion never appears twice.
  • Dismiss is per-session — re-opening /rules after closing the tab brings the suggestion back if the pattern still holds. The card is empty when there's nothing to suggest.
It works against your real data
The miner reads from your live shipments + orders, not a sampled snapshot. Demo orgs skip the card so the demo stays simple — real tenants get suggestions from the moment volume is non-trivial.

Each leaf in the AND/OR tree is a triple:

  • Field — order, customer or destination attribute. Most-used fields: lane (domestic / international / intra-EU / EU export / EU import — derived from your warehouse + ship-to country), destination.country, weightG, totalCents, tag, sku, channel, orderAgeHours, placedAt, itemCount, uniqueSkuCount (distinct SKUs vs sum-of-quantities for "single-line orders → small box, multi-line → large box").
  • Customer historycustomer.orderCount (n-th order for this buyer, includes the current one) and customer.lifetimeSpendCents (sum across all currencies; pair with currency eq X if you mix currencies). Useful for "first-time buyer → HOLD for review" or "lifetime spend > \$500 → SET_PRIORITY URGENT". Computed once per pass via a single Prisma aggregate.
  • Operator — eq / neq / gt / lt / between / contains / startsWith / endsWith / in / notIn / exists. Text operators are case-insensitive. Use in with a comma-separated list (e.g. ES, FR, DE, IT) instead of multiple OR groups.
  • Value — typed by field kind. Closed-set fields (like lane) render a dropdown so you pick from the legal values; everything else takes a typed input. Values are literals, not field references — typing warehouse.country is matched as the literal string, not the warehouse's country. Use the lane field for that case.

28 action types covering shipping, customs, attention + ops:

  • Shipping: SET_CARRIER (resolves to your first enabled account of that carrier when the user doesn't pick one), SET_SERVICE, SET_PACKAGE (uses one of your saved package presets — overrides dimensions + adds the empty box weight), SET_WEIGHT, SET_NEAREST_WAREHOUSE (postal-prefix scoring within the same country), SET_WAREHOUSE (explicit pick).
  • Risk: SET_INSURANCE, REQUIRE_SIGNATURE, SET_SIGNATURE_LEVEL (NONE / INDIRECT / DIRECT / ADULT), SET_HAZMAT (flips the dangerous-goods flag, mirrors to the order's hazmat panel).
  • International / customs: SET_INCOTERM (DAP / DDP), SET_BILL_SHIPPING_TO + SET_BILL_DUTIES_TO (sender / recipient / third-party with carrier account), SET_CONTENTS_TYPE (merchandise / gift / documents / sample / return), SET_NON_DELIVERY (return / abandon), SET_SIGNER_NAME (commercial-invoice signer), SET_HS_CODE + SET_ORIGIN_COUNTRY (apply uniformly across all line items — for per-SKU overrides, set Product.hsCode / Product.originCountry in the catalog instead, the customs builder reads them automatically), SET_PAPERLESS (electronic commercial invoice).
  • Attention: SET_PRIORITY (LOW / NORMAL / HIGH / URGENT — drives the Priority chip + sort on /orders), ASSIGN_TO_USER (route by email — "VIP customers → assign to sarah@", "international → mike@"), HOLD_UNTIL_DATE (flip to ON_HOLD with auto- resume; absolute ISO date OR relative offset like "+7d" / "+24h" / "+2w", released by the retention cron on the due date).
  • Workflow: ADD_TAG, REMOVE_TAG, SET_STATUS, SET_NOTES, SET_NOTIFY_CUSTOMER (toggle the channel's shipping confirmation email), NOTIFY (email / Slack / webhook).

Priority chain at label-buy time: your explicit input on the rate-shop panel → per-order customs form editsrule hintcatalog default (Product.hsCode etc.) → org / adapter default. So a rule never silently overrides a value you typed by hand on a specific order.

Order-side triggers — fire when the order itself changes:

  • ORDER_IMPORTED — fires on every channel sync + manual create + headless API push. The most common trigger.
  • ORDER_UPDATED — every update (status, tags, items, addresses).
  • STATUS_CHANGED — fires only when the status actually transitions.
  • TAG_ADDED — only on orders that gained the tag. Use this with ADD_TAG on a different rule for chained workflows (e.g. tag "needs-review" on big orders → another rule fires on TAG_ADDED to NOTIFY).
  • MANUAL — "Apply to existing orders" button + the legacy "Run now" on order detail. Bypasses the dedupe gate so you can re-fire on an order that already matched.

Shipment-side triggers — fire at downstream lifecycle moments instead of order edits. The rule still evaluates against the parent Order (destination, customer, lane, tags) but the timing is different:

  • LABEL_PURCHASED — fires immediately after a label is bought for the order. Useful for auto-printing packing slips, posting to Slack with tracking, ADD_TAG for post-ship analytics, follow-up emails. Standalone labels (no orderId) don't fire.
  • SHIPMENT_EXCEPTION — fires when carrier tracking flips a shipment to EXCEPTION (carrier webhook or 30-min tracking cron). Use to auto-tag for review, NOTIFY customer, escalate to ops.
  • SHIPMENT_DELIVERED — fires when a shipment confirms DELIVERED. Use for follow-up emails, review-request triggers, auto-close return window prep.

Replay-safe: a second ORDER_UPDATED for the same order skips rules that already fired via a prior matched audit row. Per-rule isolation: a broken rule doesn't take down the rest — its run is wrapped, the state snapshot is reverted, and an audit row records the failure visible in /audit.

Default behaviour for a new rule: it only fires on orders going forward. Existing open orders sit unchanged — use the Apply to existing orders button to backfill.

Schedule windows. A rule can carry an optional day-of-week + time-of-day window (set via the rule editor's Schedule section). Off-hours rules skip evaluation entirely — useful for "after-17:00 orders → next-day service" or "Sunday orders → HOLD_UNTIL_DATE +1d". Supports overnight windows (22:00–06:00) via start > end. IANA timezone per rule so EU + US shifts don't fight.

Real automations from real merchants. Copy the structure, swap the carrier / country / tag for yours.

1 · Default carrier per lane

Your most important rule — you ship the same way for the vast majority of orders. Encode it once, free your attention for the exceptions.

Trigger:    On order imported
When:       lane equals Domestic
Then:       Assign carrier   = USPS
            Assign service   = usps_priority
Trigger:    On order imported
When:       lane equals Intra-EU (no customs)
Then:       Assign carrier   = DHL
            Assign service   = N
Trigger:    On order imported
When:       lane equals International
Then:       Assign carrier   = UPS
            Assign service   = 65   (UPS Worldwide Saver)

2 · Customs signer for every international invoice

Without this you have to type the signer on every international order's customs form. Set once, never again.

Trigger:    On order imported
When:       lane not equals Domestic
Then:       Customs signer name = Veronica Isaac

For the actual handwritten signature image (DHL paperless trade, FedEx ETD), upload it once at /settings/customs — that's org-wide, not per-rule.

3 · Insurance for high-value orders

Trigger:    On order imported
When:       totalCents greater than 50000   (>$500 / €500)
Then:       Insurance         = true
            Signature required = true
            Signature level   = ADULT

Stack signature + insurance on the same rule — both hints land on the order, both fire at label buy. Adult signature is overkill for a $20 t-shirt; reserve it for the actually-valuable shipments.

4 · Route to the nearest warehouse

Multi-warehouse merchants: pick the closest warehouse to the customer automatically. Postal-prefix scoring within the same country (GB postcodes share 2-4 chars, US ZIPs 1-3, etc).

Trigger:    On order imported
Then:       Route to nearest warehouse

5 · DDP for UK orders (avoid surprise duty bills)

Post-Brexit, EU → GB ships need a clear duties story. DDP means you pay the duties up-front; the customer never gets a delivery surprise. Pair with Bill duties to + your own carrier account (Shipnest blocks DDP on platform carrier accounts to prevent duty-bill leakage).

Trigger:    On order imported
When:       destination.country equals GB
Then:       Set incoterm     = DDP
            Bill duties to   = SENDER
                              account = <your-UPS-account-#>

6 · Hazmat on battery SKUs

Lithium batteries need the hazmat flag for FedEx DG / UPS HazMat / DHL DGD compliance. Tag your battery SKUs once in the catalog (UN number + hazard class + packing group on the product page), then this rule trips the flag on every affected order. At label-buy time we forward the full DG declaration to UPS / FedEx / DHL — UPS HazMatPackageInformation, FedEx dangerousGoodsDetail.containers[], DHL dangerousGoods[]. USPS and Royal Mail don't accept API-side DG declarations and reject hazmat shipments at buy-time with a clear pointer at the other carriers.

Trigger:    On order imported
When:       sku contains BATT
Then:       Hazmat / dangerous goods = true
            Add tag = hazmat

7 · Notify on big orders

Slack ping for the warehouse manager when a high-value order lands so it gets handled by the senior packer.

Trigger:    On order imported
When:       totalCents greater than 100000   (>$1,000)
Then:       Add tag = vip
            Notify  channel = slack
                    target  = https://hooks.slack.com/services/…
                    message = High-value order {{orderNumber}} in queue ({{totalFormatted}})

8 · Stuck orders → on hold + alert

Catch orders that have been sitting too long (typically indicates a sync issue, missing inventory or address problem).

Trigger:    On order updated
When:       orderAgeHours greater than 48
            AND status equals AWAITING_SHIPMENT
Then:       Set status = ON_HOLD
            Add tag    = needs-review
            Notify     channel = email
                       target  = ops@example.com

9 · Standard package preset

Most of your shipments fit one of 2-3 box sizes. Encode the "default box" once and stop typing dimensions per order. Stack with a SKU condition for size-specific boxes.

Trigger:    On order imported
When:       weightG less than 1000
Then:       Set package preset = small-box   (your saved preset)

10 · Different signers for different shipping countries

Two rules with different priorities — the lower priority number runs first. Pair with Stop after this rule matches on each so they don't both fire.

Rule A — priority 50
Trigger: On order imported
When:    destination.country in ES, FR, IT, PT
Then:    Customs signer name = María García
         ☑ Stop after this rule matches

Rule B — priority 100
Trigger: On order imported
When:    destination.country in US, CA, MX
Then:    Customs signer name = John Smith
         ☑ Stop after this rule matches

By default a new rule only fires on orders coming after you save it (next channel sync, next webhook, next manual create). Existing open orders sit unchanged.

The Apply to existing button on the rule editor walks every open order in your workspace (AWAITING_SHIPMENT + ON_HOLD) and runs the rule against each. Shipped, delivered and cancelled orders are explicitly skipped — they're snapshots, the label is on the wire.

  • Confirm dialog shows the open-order count before running so you see the blast radius.
  • Processed in batches of 100. Per-order failures are counted but don't abort the sweep.
  • A rule.backfill_run audit row records the totals (processed / matched / errors).

The rule must be saved + enabled for the button to work — disabled rules don't fire on backfill either, intentionally.

The Test button on the rule editor opens a dry-run dialog. Type an order ID or order number (e.g. 1003 or #1003 — both work) and click Run.

You get back:

  • A green / red banner: matched or did not match.
  • The list of actions that would run (badge per action type) if the rule fired.
  • A per-condition trace table — every predicate's expected vs actual value, pass / fail. So when a rule doesn't match, you see which condition failed and what the actual value was on that order.

Test is read-only — no audit, no NOTIFY, no changes to the order. The rule doesn't even need to be saved yet — edit conditions / actions in place and hit Test repeatedly while you iterate.

Carriers & rates

/carriers manages your carrier accounts. Connect a new one (Connect dialog has per-carrier field specs and a test-connection step), edit the markup policy, toggle enabled, rotate the webhook secret.

Six carrier integrations: UPS, FedEx, DHL, USPS, Royal Mail, EasyPost. UPS supports one-click OAuth install when the operator has wired the env vars (paste a shipper number, consent in your UPS.com dashboard, done). The rest take a traditional credentials paste — for USPS that means registering a USPS Developer app at developers.usps.com and pasting the Consumer Key + Consumer Secret.

Each carrier account has a source:

  • PLATFORM — the operator's (Shipnest) corporate UPS / FedEx accounts offered to tenants at markup. Visible in the rate-shop panel with a pink Platform badge + shield icon. Subtitle shows base $X · fee $Y for transparency.
  • CUSTOMER — the tenant's own carrier contracts. No badge, no fee — passes through at the carrier's native price.
Shopify-installed workspaces use CUSTOMER rates only
If you installed Shipnest from the Shopify App Store, you connect your own carrier accounts and pay your carriers directly — platform rates (and the prepaid wallet that funds them) aren't offered, so your subscription stays 100% on Shopify Billing with nothing billed off-platform.

When the operator wires the platform env vars (UPS_* / FEDEX_* + account numbers) on Railway, every new signup auto-gets PLATFORM CarrierAccount rows. Existing orgs pick them up via the one-click backfill at /admin/carriers.

DDP on PLATFORM is blocked
International shipments with billDutiesTo: SENDER or incoterms: DDP are rejected at buy-time on PLATFORM accounts — duty bills would land on the operator's account weeks after delivery with no path to invoice the tenant back. Switch to DAP (recipient pays at customs), use third-party billing with the tenant's own account number, or connect your own carrier on /carriers and route this order to it.

The rate-shop Advanced panel reads which quote you have selected and surfaces the right banner: a red error on a Platform rate (the buy will be rejected), a green confirmation on a customer rate (duties bill against your contract 2-6 weeks after delivery), or an amber generic warning when no quote has been picked yet. "Recipient" is hidden from the Bill duties dropdown when DDP is selected (it would contradict the carrier-level pre-pay), and the Buy button stays disabled while a third-party billing entry is missing its account number.

Default markup applies to PLATFORM rates only: PLATFORM_MARKUP_BPS=500 (5%) + PLATFORM_MARKUP_FLAT_CENTS=50 ($0.50 fixed). Per-org override on Organization.markupBps + markupFlatCents, edited from /carriers.

CUSTOMER-source accounts pass through at cost — operators cannot mark up rates from a contract they don't own.

Platform-rate labels (postage + markup) are paid from your prepaid shipping wallet at buy time — see that section for funding + per-currency balances.

/rate-cards is for mid-market merchants with their own UPS / FedEx contracts at 35–45% off public rates. Upload a flat CSV (to_country, weight_to_g, price_cents) per card and the contract price surfaces in rate-shop alongside live public quotes — tagged Contract.

Informational only. Labels still buy through a connected CarrierAccount. The card's purpose is to show the gap between your real cost and the public published rate so margin is visible across the dashboard.

Channels

/integrations manages connected sales channels. Eight supported: Shopify, Amazon, Etsy, eBay, WooCommerce, BigCommerce, Squarespace, Magento.

One-click OAuth install for the five that support it: Shopify, Amazon, Etsy, eBay, WooCommerce. The tenant clicks Connect, approves on the channel's site, and the redirect-back persists encrypted tokens with no field-paste in between. Etsy's one-click path requires their app review (slow / sometimes declined for multi-channel tools); when our platform app isn't approved, sellers register their own Etsy Developer app and paste its keystring in the manual install form — see the Etsy section below.

Connected store row shows: channel + display name + shop identity (the actual creds.shop / siteUrl / storeHash) + Connected / Auth-failed badge + last synced timestamp. Inline buttons: Sync now (manual pull), Test (re-test connection, auto-re-enable on success), Disable, Delete.

Advanced menu consolidates secondary controls: Rename, Rotate webhook secret, Inspect webhooks (Shopify only — GET /webhooks.json with mismatch detection), Re- register webhooks (Shopify), Enable/Disable checkout rates (Shopify Carrier Service toggle).

The most-integrated channel:

  • One-click OAuth install with the operator's Partner App. Token rotation enabled — Shipnest handles the 24h refresh-token cycle automatically.
  • Webhooks registered at install for orders/create, orders/updated, orders/paid, orders/cancelled, refunds/create, inventory_levels/update, customers/update, app/uninstalled. HMAC verified per-request against the app client secret (multi-secret — tries app secret + env + per-store).
  • Inventory mirror: when the merchant edits stock in Shopify admin, Shipnest's Product.stock follows. Closes the over-sell race when the channel ledger drifts from our mirror.
  • Customer-update mirror: buyer profile edits in Shopify admin flow to Customer.email/name without waiting for the next order.
  • Refund webhook dedup: merchant refunding directly in Shopify creates a Return row with externalRefundId so 48h of retry attempts don't spawn duplicate rows. SKU detail captured for finance reconciliation.
  • Pre-order / backorder auto-hold: orders tagged "preorder" / "backorder" or with line items prefixed "Pre-order:" land ON_HOLD so the operator doesn't print labels before stock arrives.
  • Multi-location routing: NY Shopify location routes to Shipnest NY warehouse automatically when configured at /settings/channel-locations. Discovery from already-synced orders, optimistic save.
  • Fulfillment uses the modernfulfillment_orders flow (API 2023-10+). Tracking numbers post back to the storefront on label buy.
  • Migrate tokens button on the row appears when a store still holds a legacy non-expiring offline token (Shopify deprecated these in 2025). One click re-runs OAuth in place — order history + rules + webhooks all stay linked.

Each channel has its own connect flow tuned to the platform's API quirks. Order pulls run on the/api/cron/sync cron (every 10 min) plus real-time webhooks where supported. Stock sync flows outbound on every label buy.

  • Amazon — SP-API one-click install. Marketplaces auto-detected from the refresh token; no picker. Fulfillment via Amazon Feeds API.
  • Etsy — OAuth 2.0 PKCE when our platform app is approved by Etsy (one-click install, shop ID auto-derived). Etsy reviews multi-channel apps slowly and sometimes declines, so the manual install path also accepts a per-tenant Etsy app keystring: register your own app at etsy.com/developers/your-apps, paste the keystring + your shop ID + the OAuth refresh token. Etsy approves single-shop apps far more readily than multi-channel ones. Either path uses the same delta filter (min_last_modified) so a stale backlog doesn't burn rate budget.
  • eBay — OAuth auth-code, 18-month refresh token. Username derived via commerce.identity.
  • WooCommerce — "auto-auth" flow: tenant pastes site URL, gets redirected to wp-admin, approves there, keys come back to us via callback.
  • BigCommerce / Squarespace / Magento — manual paste of API credentials. Webhook signing supported where the platform offers it.

Cancel-on-void coverage. When you void a label, Shipnest pushes a cancel back to the source channel so the merchant's order view stops claiming the parcel shipped. Native cancel works on Shopify, WooCommerce, BigCommerce, Squarespace and Magento. Etsy, eBay and Amazon don't expose a cancel API at all — their adapters return "skipped" with a clear reason and you finish un-shipping from the channel admin yourself. Either way the audit log on /audit records exactly what happened.

When toggled on (Advanced → Enable checkout rates), your rates appear natively inside Shopify checkout as shipping options — not post-facto in /orders. The /api/shopify/carrier-service callback runs rate-shop on every checkout rate request and returns the winning quotes.

Opt-in per store. Merchants with existing Shopify Shipping profiles aren't surprised by new options appearing. The state is persisted on Store.shopifyCarrierServiceId; Disable issues the DELETE on Shopify's side.

Operations

Warehouses

/warehouses

/warehouses manages ship-from locations. Multi-warehouse merchants use the SET_NEAREST_WAREHOUSE rule action to auto-route orders by destination postal prefix (same-country match required, longest shared prefix wins). Per-warehouse PrintNode printer routing ensures the label prints at the right dock.

4-tile KPI strip above the warehouse grid: Warehouses count · Default set (Yes / None — orange when missing so you notice) · Countries (multi-region tells you when the fleet has crossed borders) · Shipments this month across the fleet. Hidden on empty workspaces so the create-first-warehouse CTA isn't buried.

/products is the catalog: SKU + name + weight + dimensions + HS code + origin country + stock level + reorder point. The list page tells you at a glance which SKUs are ready to ship internationally and which still have gaps that will block a label buy.

Per-row Status pill names the specific next action, in lifecycle order: No weight (red — blocks every carrier, even domestic) → No HS code (orange — blocks customs) → No origin (yellow — same) → No dims (blue — blocks dim-weight calc on international) → No image (gray — cosmetic only) → Complete (green — ready for any lane). The first failing check wins, so you always see the highest-impact gap first.

Five-tile KPI strip at the top: Total SKUs · Complete count · Missing weight (red when >0) · Missing HS / origin (summed because they block the same lane) · Catalog value (Σ price × stock). Filter chips above the table — click any completeness state to narrow the list, combine multiple, "Clear filters" to reset.

Bulk fixes from the toolbar let you patch a freshly-imported channel catalog without editing each SKU: Set weight applies one grams value to N rows · Set dims applies L/W/H (cm) to every SKU that ships in the same box · Set HS code + Set origin backfill customs basics · Link stock folds aliases into a master SKU so two listings for the same physical product share one stock pool · Delete removes N SKUs from the catalog in one click. Rows that are part of a bundle or referenced by historical orders skip automatically — the toast shows the breakdown ("Deleted 12 · 3 blocked"), so deleting an import that turned out to be wrong takes seconds instead of opening each row.

Master & alias SKUs. The same physical product often lives under different SKUs across channels (your warehouse calls it TEE-BLK-M, Etsy listed it as ETSY-TEE-BLK-M, Amazon as AMZ-TEE-BLK-M). Linking those listings to a single master folds them into one stock pool — a sale on any of them decrements the master's inventory. Aliases show an alias of … badge inline on the SKU column so you can tell at a glance.

Make master per-row. Got the wrong master first time? Click Make master on any alias row to swap roles — the current master becomes an alias of this row, every sibling alias re-points at this row, and the pooled stock moves with the promotion. One click instead of unlinking the whole group and re-linking with the correct master picked. Useful after a rebrand or supplier change when the canonical SKU itself changes.

New product form accepts decimal prices in your org currency (£12.50, not 1250). Dimensions go in as mm (consistent with the parcels editor); the list cell renders them back as cm (30×20×5 cm) so you don't scan an extra zero.

Bundles & kits — one SKU, N components

/products/bundles is for composite SKUs the customer buys as ONE thing on the channel but ship as multiple physical components from your warehouse (starter kits, gift sets, multi-pack promos). Each bundle is a parent Product withisBundle: true plus a list of components with quantities; stock is derived, not stored — sellable = floor(min(part.stock / part.qty)) across every component. The bottleneck part (lowest stock-to-qty ratio) is highlighted in the editor with a rose-tinted row so you know exactly which component to restock to lift the bundle's sellable count.

What the page surfaces at a glance: KPI strip (total bundles · out-of-stock count · average discount vs parts sum) · search across bundle name + SKU + every component SKU/name · per-row stock Badge ("out" / "3 kits" / "12 kits") · discount % inline (computed from parts sum vs bundle price).

Editor: SKU + name + decimal price in the org currency (Money primitive, not raw cents) · per-part stock Badge + reorder arrows · three-up recap (Parts price sum · Discount % + absolute saved · Sellable kits + bottleneck) · searchable part picker with the first 60 free components shown (out-of-stock chips in red border) · Duplicate button clones a bundle into a fresh create form so you don't re-type similar kits.

Bulk delete via checkbox. Each row in the left list has a checkbox — tick a few and a brand-tinted toolbar appears above with N selected · Delete · Clear. Bundles referenced by historical orders skip automatically; the toast shows any blockers. Component products stay untouched — only the bundle wrappers + their parts list go. Useful for nuking a season of failed kit experiments without opening each one to delete it.

Out of scope by design: bundles live only in Shipnest (the source channel sees a single SKU, not the composition) · no nested bundles · components can be any non-bundle SKU in the same org · deleting a product that's part of an active bundle is blocked with a clear error pointing at the parent.

HS code + origin country auto-fill on customs. When you set hsCode and originCountry on a product, every future international order containing that SKU gets those values pre-filled on its commercial invoice — no per- order customs form needed. Bulk-edit via the toolbar (Set HS code / Set origin) to backfill an existing catalog in a few minutes. Priority at label-buy: per-order customs form edit → SET_HS_CODE rule → Product.hsCode → undefined.

Catalog sync (opt-in per store at /products/catalog-sync) pulls the channel's catalog into Prisma daily. Off by default because it overwrites local edits.

Inventory dashboard

/inventory is the warehouse-ops cockpit. Every non-bundle, non-alias SKU becomes one row with on-hand / committed / available counts rolled up per master SKU (matches the runtime decrement logic so alias-SKU orders deduct from the master correctly). Every label buy decrements local stock and best-effort pushes the new level to the channel (Shopify, Woo, BC, Etsy, eBay, Amazon).

Five-tile KPI strip at the top: Total SKUs · Out of stock · Below reorder · Stock value (on-hand × catalog price summed across rows) · Stale SKUs (no movement in 60+ days). Stock value falls back to "—" if no SKU has a price set yet.

Per-row status falls into five buckets, each with its own badge tone and chip filter:

  • Out of stock (red) — available = 0. Banner above the table warns that channels can keep selling these unless their sync pauses listings on zero stock.
  • Low stock (orange) — available ≤ reorder point. Both the Status badge and the per-row Available cell render in orange so the number stays scannable even when Status is filtered out.
  • Stale (yellow) — last movement > 60 days. Catches dead inventory still on the books but neither shipping nor restocking — decide whether to discount, return-to-supplier or write off.
  • No reorder point (grey) — never configured for reorder alerts. Bulk-set via the toolbar.
  • In stock (green) — none of the above.

Filter chips above the toolbar narrow the table to one (or many) statuses in a click. Counts inline ("Out of stock 3 · Stale 7"). Hidden when the count is zero so the bar doesn't grow long on small catalogs. Compose with the DataTable's own search + column filters.

Restock report button appears in the toolbar when ≥1 SKU is below reorder. Exports a CSV with sku, name, on_hand, committed, available, reorder_point, suggested_qty, last_movement. Suggested qty = reorderQty when set, otherwise max(reorderPoint × 2 - available, 1)so a partial restock at least brings the SKU back above the point. Hand the CSV to procurement and they place the PO without re-typing.

Bulk receive dialog: select N SKUs + share one PO reference + per-row qty pre-filled with each SKU's reorderQty. Handles a multi-SKU supplier shipment in one go. Rows with qty = 0 skip; toast reports total units received + per-row failure count.

Per-row actions stay simple: Adjust (pencil) for a delta with a reason (adjustment / count / purchase / return / transfer) and Cycle count (repeat icon) for an absolute-count reconciliation that derives the delta.

Safe under concurrent edits. Two operators counting from different devices, or a label-buy decrementing while a manual adjustment lands at the same instant, never lose a unit. Every stock-mutating path uses Postgres row-level locking (or atomic increment / decrement on the no-clamp paths) so the ledger reflects every operation in the order it happened — no stale-read races, no silently dropped deltas.

/customers lists every distinct customer across channels (deduplicated by email). Lifetime spend per currency, last active date, tags. Click into a customer for their full order history.

The Addresses tab on the same page is your saved-address list — warehouse partners, repeat buyers, drop-ship vendors. Used as quick-pick on manual order create + return-from-sender.

Pickup scheduling goes through the real carrier APIs:

  • UPS — Pickup API v1, returns a PRN (Pickup Request Number).
  • FedEx — Pickup API v1, returns pickupConfirmationCode.
  • DHL Express — MyDHL pickups, returns dispatchConfirmationNumber.
  • EasyPost — two-call /v2/pickups + /v2/pickups/:id/buy; relays to USPS / UPS / FedEx / DHL depending on the linked carrier accounts.
  • USPS / Royal Mail — no first-class API; fall through to a local-only entry you can edit, and the operator confirms the pickup with the carrier through their portal / phone.

Three places to schedule:

  • Inline on /labels/new — a checkbox appears under Customs; tick it, set date + ready-by + close-by, the pickup fires the moment the label commits. Ship-from of any kind (warehouse, saved address, fresh address) is supported.
  • Inline in the rate-shop panel on /orders/[id] — same checkbox above the Buy buttons. Uses the order's warehouse as the pickup address.
  • Standalone at /pickups — when you want one pickup covering many labels printed earlier in the day. Also where you cancel / mark missed / mark complete.

Each pickup carries a carrierStatus flag: confirmed (carrier API acked, real confirmation number) or local (we couldn't reach the carrier or the adapter isn't wired). The badge in /pickups shows the distinction so you know whether a driver is actually dispatched or it's a manual ledger entry.

Don't schedule twice
Each click creates a new pickup. If your warehouse already has a recurring daily pickup with the carrier, leave the checkbox off and don't use /pickups — the driver comes anyway. The inline scheduling is for one-off labels or one-off addresses without a standing arrangement.

Cancelling a pickup

Click Cancel on a confirmed pickup row and we call the carrier's cancel API:

  • UPSDELETE /api/pickup/v1/pickups/{PRN}
  • FedExPUT /pickup/v1/pickups/cancel
  • DHLDELETE /pickups/{confirmation}
  • EasyPostPOST /v2/pickups/{id}/cancel

Without this, cancelling locally would leave a zombie pickup — the truck still arrives at a closed warehouse. UPS/FedEx/DHL/EasyPost now close the loop end-to-end. USPS / Royal Mail still cancel locally (no API) and you phone the carrier. The audit row carries carrierAck: cancelled | failed | skipped so post-mortems can tell what actually happened.

Bulk cancel on the pickups list — select N SCHEDULED rows and a rose-tone Cancel button appears on the toolbar. Carrier-confirmed pickups get cancelled at the carrier side too (truck won't come); locally-logged pickups just flip status. Completed pickups skip automatically. Useful when a warehouse closes early and you need to pull every outstanding pickup in one go.

Manifests at /manifests group the day's shipments per carrier with one row per parcel. The "Only handed-over shipments" checkbox defaults on so the manifest reflects what actually left, not what was just printed. Bulk delete on the manifests list removes OPEN manifests in one click; closed manifests skip automatically (they stay as historical records the carrier may still reference). Shipments attached to a deleted manifest get unlinked, not deleted.

Closing a manifest = real carrier-side close

Clicking Close calls the carrier's end-of-day API:

  • EasyPostPOST /v2/scan_forms. Relays USPS (PS Form 5630 SCAN form — REQUIRED at 25+ USPS parcels/day) and UPS/FedEx where supported. Returns a long-lived CDN URL.
  • FedEx Ground POST /ship/v1/shipments/closures. Returns the carrier's own manifest PDF as base64 (we persist it as a data: URL so it never expires). Ground accounts only; Express accounts use the FedEx Ship Manager portal's EOD ribbon.
  • UPS, DHL Express, Royal Mail — no API. Manifest closes locally; the driver scans labels individually (UPS) or you reconcile via the portal (DHL / RM).

When the API returns a SCAN form, the closed manifest shows a green Carrier confirmed badge and a SCAN form download button. Hand the PDF to the driver — they scan ONE barcode covering every parcel on the manifest, instead of every label individually. Faster pickup + earlier first-scan in tracking.

Why USPS 25/day matters
USPS requires the SCAN form when you ship 25+ parcels in a day. Without it, USPS scans every label individually at acceptance — slow, and the first tracking scan can be delayed by hours. With it, a single barcode acknowledges the whole batch at once.

CSV imports

/imports

/imports bulk-imports orders, products, or address-book entries from CSV. An aggregator-export preset auto-maps the canonical column names used by US shipping aggregators (Order #, Item Weight (oz), Address Line 1, …) and converts oz → g + decimal $ → cents server-side, so you can drop in an export from your previous tool without manual field-mapping.

Bulk import for products supports SKU + name + weight + dimensions + HS code + origin in one go.

Post-sale

/returns manages return RMAs./returns/portal is a public, branded self-serve portal: customer enters order number + email, picks items to return, gets a return label by email. Set the policy at /returns/policy.

Lifecycle states: REQUESTED → APPROVED → LABEL_ISSUED → IN_TRANSIT → RECEIVED → REFUNDED (or DENIED). Each transition fires a return.* outbound webhook for downstream automation.

Bulk delete via the toolbar. Select N rows and the swap-on-selection bar surfaces a Delete button. Only REQUESTED + DENIED RMAs are eligible; active returns (APPROVED / IN_TRANSIT / RECEIVED / REFUNDED) skip automatically and stay as historical records. Useful for cleaning up portal-test entries or DENIED cancellations without opening each one.

Issuing a label

The Issue return label dialog mounts a service-code dropdown filtered to whatever the picked carrier account supports — UPS Ground / Worldwide Saver, FedEx Ground / International Priority, USPS Ground Advantage, DHL Express Worldwide, Royal Mail Tracked 24/48, EasyPost mappings. No more typing "03" or "FEDEX_GROUND" by hand.

QR-code returns (no printer needed)

When you issue a return label from the return detail page, the dialog carries a QR-code return checkbox. Toggle it (UPS or FedEx only) + paste the customer's email and the carrier emails them a link with a QR they show at any drop-off counter — UPS Access Point, FedEx Office, or in the case of FedEx Email Return Label any FedEx location. No printing on the buyer's side; the email arrives directly from the carrier, not from us. Other carriers (DHL, USPS, Royal Mail, EasyPost) silently fall through to a regular PDF label.

/claims is the claim filing UI. From the shipment detail, click File a claim — pre-fills shipmentId + carrier + currency. Toggle file against third-party policy when the shipment has an active Shipsurance / XCover policy and the claim should go there instead of the carrier.

Bulk delete on the list page (select N rows → Delete button on the swap-on-selection toolbar). Only DRAFT + DENIED claims are eligible; active claims (SUBMITTED / UNDER_REVIEW / APPROVED / PAID / CLOSED) skip automatically and stay as historical records. Use for cleaning out half-typed drafts.

Filing through the carrier API

Click File with carrier (or File with insurer for policy-backed claims) and we call the real API:

  • UPS POST /api/claims/v1/claims. Returns the UPS ClaimNumber.
  • FedExPOST /case/v1/cases with caseType CLAIM. Returns case number + case id.
  • ShipsurancePOST /v3/claims against the policy id from your original purchase.
  • XCover POST /partners/v1/bookings/{policyId}/claims.
  • USPS, DHL Express, Royal Mail — no first-class API. Falls through to a local-only record; finish on the carrier portal. The audit trail + lifecycle still live in /claims either way.

When the API ack's, the claim shows a green Filed with carrier / Filed with insurer badge with the provider claim number in tooltip. When the API fails (transient, unimplemented carrier), the fallback amber Local only badge appears + a toast reminding you to finish via portal.

Uploading supporting documents

Once the claim is filed, the evidence card shows an Upload supporting documents to the carrier section. Pick files from disk — damage photos, invoice, packing slip — choose a category per file, click Send. We read the bytes in the browser, base64- encode, and pipe straight to the provider's attachment endpoint:

  • UPSPOST /api/claims/v1/claims/{id}/documents
  • FedExPOST /case/v1/cases/{caseId}/attachments
  • ShipsurancePOST /v3/claims/{id}/documents
  • XCoverPOST /partners/v1/claims/{id}/attachments
  • USPS / DHL / Royal Mail — no API; upload via portal

Caps: 5 MB per file, 10 files per upload, image/jpeg + image/png + image/heic + application/pdf + text/plain only. The carrier may still reject individual files (wrong MIME, OCR can't read the photo); the result toast shows "Uploaded N of M — carrier rejected (M-N)" so partial success is visible.

We never store the bytes
The base64 bytes flow through the browser → server action → carrier API chain in a single request. Once processed, they're released from memory — no S3, no temp tables, no audit-row payload (audit only records file count + filenames). Avoids hosting cost + GDPR / data-retention concerns + scaling problems if a tenant uploads heavy multi-MB photo bursts.

Lifecycle: DRAFT → SUBMITTED → UNDER_REVIEW → APPROVED / DENIED → PAID → CLOSED. Each transition lands in the notifications bell + outbound webhook (claim.created, claim.paid).

/track/<tracking-number> is a public tracking page styled with your logo + brand colour + support email + storefront URL. The "Powered by Shipnest" credit in the footer is non-removable; everything else swaps to your identity.

When the shipment's associated return has been REFUNDED with a non-zero refund amount, an emerald "Refund processed" panel appears with the amount + date + a 5-10 business day card-settlement note.

/reports/voids-and-refunds aggregates three sources: successful voids (carrier refunds full label cost), past-window void rejections (you tried but the carrier's refund deadline elapsed), and customer refunds on returned orders. CSV export available.

Per-carrier deadlines: UPS 90d, FedEx 60d, DHL 28d, USPS 28d, Royal Mail 14d, EasyPost 30d. The shipment detail greys out the Void button + tooltip when past-window.

International

International orders get a Customs form on the order detail. Fields: contents type (merchandise / gift / documents / return / sample), non-delivery option (return / abandon), declared currency (defaults to order currency), per-line items with HS code + origin country + value + weight, optional EEL / PFC for US-origin exports, paperless trade toggle and signer name.

Form edits persist to Order.customFields._customs and the buy-label flow merges them with rule hints + org-level customs profile. Adapters forward to DHL electronic invoice, FedEx ETD, UPS InternationalForms.

International tax registration numbers configured at /settings/customs: IOSS (low-value EU imports under €150), GB EORI, NI EORI, EU EORI, GB VAT.

At label-buy time, resolveTaxIds picks the right number per lane:

  • IOSS — non-EU origin → EU destination, value ≤ €150. Stamped on the invoice so VAT was collected at checkout; customs clears without buyer-side charge.
  • EORI — GB-origin → GB EORI; EU-origin → EU EORI.
  • VAT — GB-origin → GB VAT.

The resolver is B2C-shaped (DDP duty accounts and B2B VAT reverse-charge stay operator-driven via per-order overrides).

Run a US Shopify + UK Shopify + EU Etsy on one Shipnest org — every order keeps its native currency from the channel. Dashboards stack $2,000 · £900 · €800 side by side rather than collapsing to a fake FX-converted single number.

The defaultCurrency in /settings is only the fallback for manual orders without explicit currency and for the product catalog. It does NOT override the channel-set currency of synced orders.

Reports

Analytics

/analytics

/analytics is the deeper dashboard:

  • Spend (30d) per currency stack, with week-over-week delta.
  • Avg cost / label per currency.
  • Delivered ≤ 5d rate.
  • CO₂e (30d) aggregate (less = better, delta semantics flipped).
  • Carrier performance table — shipments + spend + delivered rate + avg days + claims per carrier.
  • Channel volume share + per-channel spend.
  • Top lanes — country-pair routes ranked by volume, with avg cost + total spend per currency.
  • Heatmap — orders by hour-of-day × day-of-week.

/reports manages CSV reports emailed on schedule (daily / weekly / monthly). Pick the kind: orders, shipments, voids-and-refunds, marketing-mix, custom carrier breakdown. Set recipients, send time and cadence; /api/cron/reports generates and sends.

Ad-hoc one-shot exports available at /api/reports/...csv for CSV download without scheduling.

Audit log

/audit

/audit is the full log of every mutation inside your org: label buys, voids, rule firings, store connects, webhook rotations, role changes. Filter by actor, action, entity. CSV export.

Rule audit rows include rule.matched (which rule fired against which order), rule.failed (rule threw — message + name aggregated in the notification bell), and rule.hint_overwritten when two rules target the same hint.

Every shipment gets a CO₂e estimate based on weight × transport mode (road ~100 g/kg, air ~600 g/kg, calibrated to DEFRA 2023 parcel-delivery factors). Aggregate on/analytics; column on /shipments (hidden by default — reveal via column chooser).

Mode classifier reads carrier + service name + lane. DHL is always classed as air. International cross-border defaults to air unless the service name explicitly says ground.

Settings

Org & team

/settings

/settings is the org admin surface. Under the Profile card: organization name, slug (used in public URLs like /returns/portal?org=<slug>), default currency, weight unit, dimension unit.

Team subsection invites Owner / Admin / Manager / Member / Viewer roles. Role rank: Owner > Admin > Manager > Member > Viewer. RBAC gates every mutation server-side.

Bulk remove on the team list — select N members and a rose-tone Remove button appears on the toolbar. Every RBAC gate from the per-row remove re-applies server-side per id: can't self-remove, can't remove anyone at or above your rank, can't remove the last OWNER. The toast surfaces the per-id reasons ("Removed 8 · 2 skipped: 1 self, 1 above rank") so you see what was kept + why. Useful for batch-removing a departing team after a contract ends.

Branding

/settings/branding

/settings/branding swaps the default Shipnest chrome on public surfaces (/track, /returns/portal) for your identity: logo, brand colour (drives the gradient + accent), support email (lands in the public footer), storefront URL (becomes the "Back to {store}" button). Live preview before save.

Security & 2FA

/settings/security

/settings/security stacks three sign-in methods that cooperate rather than compete:

  • Password — required, scrypt-hashed, rotated transparently from legacy SHA-256 on first login. Resetting the password invalidates every session issued before the rotation so a stolen cookie dies instantly.
  • TOTP (RFC 6238) — opt-in for tenant users,mandatory for any user with the platform-admin flag. Enrol with any authenticator app. Disabling requires a current valid code so a hijacked session can't turn it off.
  • Passkeys (WebAuthn) — Touch ID, Face ID or hardware keys (YubiKey, Google Titan). Add as many as you need (one per device); sign in from /login with one tap, no email field required (the picker shows discoverable credentials bound to the Shipnest domain). Removing a passkey doesn't lock you out of the password / TOTP fallbacks.

Privileged actions (buy label, void label, change markup, delete account, delete order) require role gates server-side regardless of which method authenticated the session. Login surfaces are rate-limited per IP so brute-force spraying gets 429'd well before any real damage.

API keys

/settings/api

/settings/api issues sk_live_* keys for the public REST API. Show-once secret on creation; tenants store it themselves. Revoke any time — the key's SHA-256 hash is what we persist (never the plaintext), so a leaked key doesn't leak past its first authenticated call.

Bulk revoke when a developer secret leaks into a public repo and you need to invalidate every active key at once. Select N rows and the Revoke button on the toolbar marks all of them revokedAt = now() in one transaction. Already-revoked keys skip automatically. Keys are marked revoked (not deleted) so the historical lastUsedAt forensics + audit trail stay intact — issue fresh keys to restore access.

See /docs for the full REST API reference: rate-shop, label buy, void, list shipments, list orders, tracking, address validation, insurance quote, plus the carrier-accounts discovery endpoint.

Outbound webhooks

/settings/webhooks

/settings/webhooks registers HTTPS endpoints for Shipnest to POST events into. Subscribe per-event: shipment.label_purchased, shipment.voided, shipment.delivered, return.created, return.refunded, claim.created, claim.paid.

Every delivery is HMAC-SHA256 signed with the webhook's secret over a timestamp-prefixed body (timestamp.body). Verify on your side by recomputing the HMAC against the raw bytes — replay- safe because the timestamp is part of the signature input.

/settings/webhooks/deliveries is the per-delivery audit trail with retry button on failed attempts. Bulk retry on the toolbar — select N FAILED or PENDING rows and a Retry button surfaces; DELIVERED rows skip automatically so you never re-fire a duplicate event into your tenant's endpoint. Useful when an endpoint went down for a few hours and dozens of failed attempts piled up — retry the lot in one click. Sequential under the hood so your endpoint doesn't get rate-limited by us.

Printers (PrintNode)

/settings/printers

/settings/printers connects a PrintNode account so labels skip the browser print dialog and go straight to your warehouse Zebra / Dymo. Multi-warehouse orgs route per-warehouse so dock A's Zebra and dock B's Zebra both work. One-click retry on failed jobs.

Without PrintNode, labels open in a new browser tab (default). The integration is strictly additive — turning it off doesn't regress any flow.

Billing

/settings/billing

/settings/billing manages your subscription: current plan, usage meters (labels this month, users, warehouses), addon cards, and invoices.

Two billing paths
If you installed Shipnest from the Shopify App Store, your subscription is billed through Shopify Billing — upgrades, downgrades, and cancellations happen in-app and are confirmed on Shopify's own screen, and every charge shows up in your Shopify admin under Settings → Billing. If you signed up directly on shipnest.app, billing is handled in-app with your saved card. The plan tiers, caps, and features below are identical on both paths.

Five plans: Starter (free, 100 labels/mo) → Growth ($39, 1,000 labels) → Pro ($69, 3,000 labels, multi-warehouse, API) → Scale ($99, 5,000 labels, unlimited users) → Enterprise (custom). 14-day free trial on every paid tier. /upgrade renders a side-by-side feature matrix when you want to compare beyond the cramped per-card view.

Annual billing. Switch the Monthly / Annual toggle on signup or in /settings/billing to save ~17% (effectively 2 months free): Growth $32.50/mo annual, Pro $57.50/mo, Scale $82.50/mo. Switch cycles any time from /settings/billing — proration is handled automatically.

What happens at the label cap. Direct (shipnest.app) accounts on a paid tier keep shipping past the monthly cap at the tier's metered overage rate — Growth $0.08, Pro $0.06, Scale $0.04 — added to the next invoice (toggle it off in /settings/billing for a hard budget ceiling). Shopify-billed workspaces and free Starter run a hard cap: past the included label count, label buys pause until the next billing period or a plan upgrade.

Addons let you scale a single dimension without upgrading the whole tier. Growth + Pro both expose a +$5/mo per extra user seat; Pro also exposes +$10/mo per extra warehouse slot past its built-in 5. Slots are proration-billed and reversible (the remove button refuses to drop you below your current teammate / warehouse count so you don't accidentally over-cap yourself).

Downgrade preview. Picking a lower tier opens a modal that surfaces cap conflicts (e.g. "you have 5 members but Growth allows 3 — remove 2 before switching") and lists every feature you'd lose before the change is applied.

Past-due grace. When a payment fails, an amber banner appears on every page counting down the days remaining in the 14-day grace window. Labels keep shipping during grace. Update your payment method to re-attempt the charge immediately instead of waiting for the regular retry schedule. After 14 days expire, label-buy locks until the invoice clears. Day-3 / day-7 / day-14 reminders go to OWNER + ADMIN inboxes. (Shopify-billed workspaces clear a past-due charge from Shopify Admin → Settings → Billing.)

Invoice CSV export. Direct (shipnest.app) accounts get an "Export CSV" button on the Invoices card that downloads every invoice on the workspace as a CSV (date, amount, status, hosted URL, PDF URL) — hand-off ready for finance / accountant. Shopify-billed workspaces find their invoices in the Shopify admin under Settings → Billing.

Shipping wallet

/settings/billing

When you ship using Shipnest's platform carrier rates (our UPS / FedEx / EasyPost accounts — the ones tagged Platform in rate-shop), each label is paid from a prepaid wallet. The wallet is separate from your monthly subscription: the plan covers the software, the wallet covers the postage.

Not applicable on Shopify-installed workspaces
The wallet only exists for direct (shipnest.app) accounts. If you installed via the Shopify App Store you ship on your own connected carrier accounts and pay your carriers directly — there's no wallet to fund.

What draws from the wallet — and what doesn't

  • Platform-rate labels debit the label cost (carrier postage + our platform fee) from your balance the moment you buy. The rate-shop panel shows the exact figure on the quote before you commit.
  • Labels on your own connected carrier accounts (your UPS / FedEx / DHL / USPS / Royal Mail contracts) are billed directly by that carrier and never touch the wallet — there's no platform fee on rates you negotiated yourself.

Topping up

From /settings/billingShipping wallet, pick a preset ($25 / $50 / $100 / $250) or type a custom amount, choose the currency, and Top up. You're sent to a secure checkout for a one-time payment; your balance updates the moment payment confirms. Only Admins + Owners can add funds.

One balance per currency

The wallet holds a separate balance for each currency your platform carriers quote in. If some platform rates come back in EUR and others in USD, you'll see a EUR balance and a USD balance side by side and top each up independently — we never convert between currencies, so what you're charged is always exactly the carrier's price plus the fee in that currency. A label in a currency you haven't funded yet is blocked until you top up that currency.

Auto-reload

So you never get blocked mid-shift, turn on auto-reload per currency: set a threshold and a top-up amount, and when that balance drops below the threshold we automatically charge your saved card to refill it. The card used is the one from your last manual top-up — so make one manual top-up first to put a card on file. If an automatic charge is declined (or your bank asks for extra authentication, which can't happen unattended), we'll skip it and you top up manually; it never blocks an order on its own.

Voids & refunds

Void a platform label within the carrier's void window and the full amount (postage + fee) is credited straight back to your wallet balance — no waiting for a separate refund. Every top-up, label charge, and void refund is listed in the wallet history table with a running balance.

Keep a buffer
If your balance can't cover a platform label, the purchase is blocked before any carrier charge — you'll be prompted to top up. A low-balance banner appears across the app once a funded currency runs low so you can refill before it interrupts a shift. (Labels on your own carrier accounts are unaffected.)
Operator (admin)

/admin is the operator-only surface (gated by User.isPlatformAdmin). TOTP required on every sign-in.

  • /admin/organizations — list every tenant org with Healthy / At risk / Critical badge derived from churn signals (sync failures, webhook failure rate, no shipments in 14d, billing past-due, label cap near, missing default warehouse).
  • /admin/users — every user across orgs.
  • /admin/stores — store health roll-up.
  • /admin/carriers — every carrier account across orgs + the platform-rates backfill button.
  • /admin/print-jobs — global PrintNode job feed.
  • /admin/wallets — tenant prepaid-wallet balances + total float per currency; adjust / refund a balance.
  • /admin/setup-guide — step-by-step credential recipes per integration (Shopify, Amazon, eBay, Etsy, UPS, FedEx, EasyPost, Stripe).
  • /admin/legal — registered legal identity, doc versions, compliance check.
  • /admin/impersonation — assume any org for support without being a member; banner renders across the app while the cookie is set.

Platform settings

/admin/settings

/admin/settings is the read-only env-var dashboard. Shows which secrets are wired (masked) and groups them by purpose: Platform rate source (monetisation), Email, Store OAuth, Other carrier OAuth, Sentry, Insurance, Address verification, Stripe billing, Webhook signing.

Billing health

/admin/billing

/admin/billing aggregates plan state across orgs: MRR, plan distribution, status distribution (active / trialing / past-due / canceled), renewing-in- 7-days list, plan bars with per-plan MRR, "Needs attention" (past-due + incomplete + canceled) and "Cap risk" (orgs ≥ 80% label usage).

Cron heartbeats

/admin/crons

/admin/crons shows the last successful run of each cron (sync, tracking, reports, catalog-sync, demo-reset, dunning) with status + duration + age. Read from CronHeartbeat rows written by thewithCronHeartbeat wrapper around each cron endpoint.

Public /status page surfaces the same data for monitoring tools — overall ok / degraded / down based on DB latency + 15-min error floor + cron freshness.

Support inbox

/admin/inbox

/admin/inbox aggregates every chat conversation, both from the marketing site (anonymous visitors) and from inside the app (logged-in tenants). Three tabs:

  • Active — bot is replying, no human action needed.
  • Escalated — bot couldn't answer, visitor asked for a human, AI provider erred, or the thread crossed 20 turns. Red badge counter in the sidebar so you spot new ones at a glance.
  • Resolved — manually marked closed by an operator.

Click any conversation for the full thread. Four actions in the toolbar:

  • Take over — pauses the AI; from now on every reply is yours.
  • Send — types as the operator. Auto-pauses the AI as a side effect (a Reply that fights the bot is a UX bug).
  • Return to AI — un-pauses. The next visitor message gets the AI again.
  • Mark resolved / Reopen — lifecycle.

Every transition writes a SYSTEM message into the thread so the inbox + the visitor widget show "operator joined" / "AI resumed" / "marked resolved" inline. Escalations also fire an email to SUPPORT_INBOX_EMAIL (defaultinfo@shipnest.app) with a one-click link back to /admin/inbox/[id] so you don't miss anything when you're away from the dashboard.

Support AI

/admin/ai

/admin/ai carries two knobs for the chat bot:

  • FAQ Markdown — concatenated to the system prompt alongside the auto-imported product overview. Use this for things that aren't in /guide: pricing exceptions, sales objections, deployment-specific quirks, escalation rules. Saves invalidate the in-memory knowledge cache so a hot conversation picks up the edit on the very next reply.
  • Bot kill switch — toggle the bot off entirely. When disabled, every visitor message routes straight to the inbox without an AI reply (escalation is automatic). Useful while you debug, change the prompt, or temporarily prefer human-only support.

Without OPENAI_API_KEY on the deploy, the widget still creates conversations and routes them to the inbox — every message auto-escalates so nothing gets dropped. The page shows an amber warning when the env var is missing.

Support

A floating chat widget lives in the bottom-right of every app page. AI-powered support trained on this exact guide: ask anything about pricing, integrations, automations, returns, or how to do X. The bot answers in plain English, with links to the relevant settings page when applicable.

  • The bot knows your account. Tenant chats automatically pass your workspace name, plan, and the logged-in user's email so answers can be tier-aware (and so an escalation to support arrives with context, not "anonymous").
  • Escalates when it's out of depth. Type "talk to a human", "this is a bug", "I need a refund" — anything the bot can't confidently answer — and a human picks it up from /admin/inbox. You see "Escalated — a human will reply shortly" inline; replies surface back into your widget without a refresh.
  • The same bot is on the marketing site. Visitors without an account get an anonymous variant — same product knowledge, plus an inline form so they can leave an email when they escalate.
Operators read every escalation
An escalation isn't a black hole — it triggers an email to our support inbox so a real person picks it up, usually within a business day. Reply through the widget and you'll see whoever responded as a green bubble (vs the AI's purple-gradient avatar).