SFIB Auto-Batcher
Context
Today batching is manual: export CSV from ShipStation, run sfib_split.py to slice by hand-entered cutpoints, then re-import. The user wants an automatic tool that pulls unshipped orders directly from ShipStation, groups them using their cascading volume strategy, and pushes batches back to ShipStation as real Batch objects — no CSV middleman.
Batching Heuristic
Two distinct passes, in this order:
Pass 1 — Cost/volume pass (spans ALL unshipped orders, no date filter)
- Pull every unshipped order regardless of age.
- Tally SKU frequency across the entire pool.
- Cascade by volume tiers — orders dominated by the highest-volume SKU first (≥400), then ≥300, then ≥200. Each tier becomes one batch of "uniform" orders.
- Around the ~100 threshold, switch from SKU tiers to product-type groupings (seed bag vs. shaker, etc., reusing
PRODUCT_MAPfrom sfib-analyzer).
Pass 2 — Interval pass (only on what's left)
- After Pass 1 claims its orders, prompt the user for an interval: type a single number like
7for 7-day buckets, or enter a custom date range. Default:7. Remaining orders get split into one batch per interval, oldest first.
The script must be deterministic enough to avoid surprises but match this layered logic.
Critical Research Findings
- ShipStation runs two APIs in parallel. v2 (
api.shipstation.com) has/v2/batchesbut no orders endpoint. v1 (ssapi.shipstation.com) has/ordersbut limited batch support. This tool needs both.- v1:
GET /orders?orderStatus=awaiting_shipmentto pull unshipped orders + line items - v2:
POST /v2/batcheswithshipment_idsto create batches;POST /v2/batches/{id}/addto append;POST /v2/batches/{id}/process/labelsto process
- v1:
- v1 orders ↔ v2 shipments mapping is the hard part. v2 batches expect
shipment_ids(se-...), but v1 returns order IDs. Bridge viaGET /v2/shipments?shipment_status=pendingand match on order number. - No first-party Python SDK for v2. Use
requestsdirectly. Community v1 SDKs exist (groveco/shipstation-python-client,pyshipstation) but they're v1-only. - Rate limit: 200 req/min. Pagination: 500/page. Honor
X-Rate-Limit-Remainingheader and back off. - No v2 sandbox. Dry-run mode is essential.
- SKU filtering is client-side. Neither API filters shipments by SKU; fetch then group locally.
Approach
Single-file Python CLI: sfib_batcher_1.0.py in a new Batch API/ folder (sibling to Batch Split/). No external deps beyond requests and the stdlib. Follows the same versioning convention as SFIB-Analyzer-2.0/CLAUDE.md (never edit-in-place, always copy to a new minor version).
Auth — macOS Keychain
Two keys live in the login keychain:
security add-generic-password -s shipstation_v1 -a sfib -w
security add-generic-password -s shipstation_v2 -a sfib -w
At runtime the script shells out to security find-generic-password -s shipstation_v1 -a sfib -w and reads stdout. No .env, no plaintext on disk. Fail loudly with the exact security add-generic-password command if either key is missing.
Module Layout
Single file, but logically separated:
- KeychainAuth — fetch v1 + v2 keys.
- ShipStationClient — small wrapper around
requests.Sessionwith separate base URLs + auth headers for v1 and v2, automatic pagination, rate-limit handling, and methodslist_orders_v1,list_pending_shipments_v2,create_batch_v2,add_to_batch_v2,process_batch_v2. - OrderShipmentLinker — fetch v1 orders + v2 pending shipments, build a
{order_number: shipment_id}map. Warn (don't crash) on orphaned orders. - BatchPlanner — pure function, no API calls. Takes
(orders, settings), returns[BatchPlan]. Unit-testable and dry-runnable. - BatchExecutor — calls
create_batch_v2per plan. Skips empty plans. Never auto-processes — labels cost real money, requires explicit--process. - main() — interactive prompts mirroring the analyzer's UX.
Planner Formulas (Configurable Constants)
SKU_TIER_THRESHOLDS = [400, 300, 200, 100] # cascade
SKU_DOMINANCE_RATIO = 0.6 # an order is "uniform" for SKU X if X is ≥60% of its line items
PRODUCT_GROUP_FALLBACK_THRESHOLD = 100 # below this SKU count, switch to product-type grouping
DEFAULT_INTERVAL_DAYS = 7 # used by Pass 2 if user just hits Enter
MAX_BATCH_SIZE = 250 # ShipStation soft limit
Algorithm
Pass 1 — cost/volume (entire unshipped pool)
- Compute SKU frequency across ALL unshipped orders (count of orders containing each SKU, weighted by line quantity). No date filter.
- Walk thresholds high→low. At each tier, find SKUs whose count meets the threshold and aren't yet "claimed." For each such SKU, pull all orders where that SKU is dominant (
SKU_DOMINANCE_RATIO); mark those orders claimed; emit oneBatchPlanper SKU with reason"Tier ≥{N}: {sku}". - Once the active threshold drops below
PRODUCT_GROUP_FALLBACK_THRESHOLD, switch to product-type grouping usingPRODUCT_MAPfromsfib-analyzer-2.13.py.
Pass 2 — interval (everything still unclaimed)
- Prompt:
Interval for remaining orders [Enter=7 days, or N for N days, or YYYY-MM-DD..YYYY-MM-DD for a range]:- Empty input → 7-day buckets
- Single integer N → N-day buckets
start..end→ one batch covering exactly that range; orders outside stay unbatched and get reported
- Sort remaining orders by
orderDateascending. Walk forward in N-day windows starting from the oldest order's date, emitting oneBatchPlanper window. Skip empty windows. - Always print the full plan before any write. Refuse batches >
MAX_BATCH_SIZE— split into numbered sub-batches (Tier ≥400: SKU-X (1/3)).
Files To Create
| Path | Purpose |
|---|---|
Batch API/sfib_batcher_1.0.py | The script |
Batch API/CLAUDE.md | Same versioning rules as analyzer (chmod 444 on release) |
Batch API/README.md | Setup steps: keychain commands, first-run checklist |
Files to read (not modify) at implementation time
SFIB-Analyzer-2.0/sfib-analyzer-2.13.py— for the canonicalPRODUCT_MAPand run-loop UX patternSFIB-Analyzer-2.0/CLAUDE.md— versioning rules to mirror
Verification
- defaultDry-run mode: prints the plan as a table (
Batch | Reason | Orders | Sample order #s), makes ZERO v2 writes. User must explicitly pass--committo actually callPOST /v2/batches. - smoke
--list-only: hitsGET /ordersv1 +GET /v2/shipmentsv2 and dumps counts. Confirms both API keys work without touching state. - first run
--limit 1: creates exactly one tiny batch (smallest tier). Verify in the ShipStation UI, then bail. - gated
--processis the only way to trigger label purchases. Default leaves batches unprocessed for UI sanity-check. - safetyLinker sanity check: print orphan counts before planning. If >5% orphaned, abort.
Out Of Scope For v1.0
- Auto-processing batches into labels (manual UI confirm only)
- Touching
sfib_split.pyor any analyzer script - Storing a local cache/DB (re-fetch each run; rate limits make this fine)
- v2 sandbox support (doesn't exist yet per ShipStation docs)