SFIB Auto-Batcher

ShipStation API tool — plan reference · saved 2026-04-06

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)

  1. Pull every unshipped order regardless of age.
  2. Tally SKU frequency across the entire pool.
  3. 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.
  4. Around the ~100 threshold, switch from SKU tiers to product-type groupings (seed bag vs. shaker, etc., reusing PRODUCT_MAP from sfib-analyzer).

Pass 2 — Interval pass (only on what's left)

  1. After Pass 1 claims its orders, prompt the user for an interval: type a single number like 7 for 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/batches but no orders endpoint. v1 (ssapi.shipstation.com) has /orders but limited batch support. This tool needs both.
    • v1: GET /orders?orderStatus=awaiting_shipment to pull unshipped orders + line items
    • v2: POST /v2/batches with shipment_ids to create batches; POST /v2/batches/{id}/add to append; POST /v2/batches/{id}/process/labels to process
  • v1 orders ↔ v2 shipments mapping is the hard part. v2 batches expect shipment_ids (se-...), but v1 returns order IDs. Bridge via GET /v2/shipments?shipment_status=pending and match on order number.
  • No first-party Python SDK for v2. Use requests directly. 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-Remaining header 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:

  1. KeychainAuth — fetch v1 + v2 keys.
  2. ShipStationClient — small wrapper around requests.Session with separate base URLs + auth headers for v1 and v2, automatic pagination, rate-limit handling, and methods list_orders_v1, list_pending_shipments_v2, create_batch_v2, add_to_batch_v2, process_batch_v2.
  3. OrderShipmentLinker — fetch v1 orders + v2 pending shipments, build a {order_number: shipment_id} map. Warn (don't crash) on orphaned orders.
  4. BatchPlanner — pure function, no API calls. Takes (orders, settings), returns [BatchPlan]. Unit-testable and dry-runnable.
  5. BatchExecutor — calls create_batch_v2 per plan. Skips empty plans. Never auto-processes — labels cost real money, requires explicit --process.
  6. 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)

  1. Compute SKU frequency across ALL unshipped orders (count of orders containing each SKU, weighted by line quantity). No date filter.
  2. 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 one BatchPlan per SKU with reason "Tier ≥{N}: {sku}".
  3. Once the active threshold drops below PRODUCT_GROUP_FALLBACK_THRESHOLD, switch to product-type grouping using PRODUCT_MAP from sfib-analyzer-2.13.py.

Pass 2 — interval (everything still unclaimed)

  1. 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
  2. Sort remaining orders by orderDate ascending. Walk forward in N-day windows starting from the oldest order's date, emitting one BatchPlan per window. Skip empty windows.
  3. 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

PathPurpose
Batch API/sfib_batcher_1.0.pyThe script
Batch API/CLAUDE.mdSame versioning rules as analyzer (chmod 444 on release)
Batch API/README.mdSetup 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 canonical PRODUCT_MAP and run-loop UX pattern
  • SFIB-Analyzer-2.0/CLAUDE.md — versioning rules to mirror

Verification

  1. defaultDry-run mode: prints the plan as a table (Batch | Reason | Orders | Sample order #s), makes ZERO v2 writes. User must explicitly pass --commit to actually call POST /v2/batches.
  2. smoke--list-only: hits GET /orders v1 + GET /v2/shipments v2 and dumps counts. Confirms both API keys work without touching state.
  3. first run--limit 1: creates exactly one tiny batch (smallest tier). Verify in the ShipStation UI, then bail.
  4. gated--process is the only way to trigger label purchases. Default leaves batches unprocessed for UI sanity-check.
  5. 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.py or 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)

Sources