Skip to content

ebay-store

Superseded in part (2026-09-14): eBay's Inventory API is no longer the system of record. Angel decided InvenTree is master for stock and every channel — eBay, Amazon, any future store — is pushed to and pulled from through a channel adapter. See company-end-state. The rest of this note stands, including "do not build a second inventory database".

Bash scripts that list and manage an eBay store through the Sell APIs. Selling embedded-electronics parts (LiPo cells, modules) as a solo seller, dozens of SKUs. Repo: ~/Documents/GitHub/ebay-store → github.com/angelEmbeddedEra/ebay-store (private)

Status

  • 2026-09-09: repo pushed to GitHub, branch mastermain. Sandbox only. Sandbox account is set up end to end: business-policy opt-in done, three policies created, merchant location created, one test listing published.
  • 2026-09-09: auth.sh done and proven live. All scripts pull tokens from it, inventory.csv and reconcile.py reconcile clean against sandbox. Nothing is blocked; the remaining work is data entry and the production cutover.

Inventory model

eBay's Sell Inventory API is the inventory system of record. Do not build a second database. It already stores SKU, title, aspects, price, and available quantity, and Seller Hub's Reports tab does bulk CSV upload with no code at all.

The local file tracks only what eBay does not know:

column why eBay can't answer it
sku join key to the eBay inventory item
qty_on_hand eBay only knows what's allocated to a listing
unit_cost cost basis for margin and taxes
bin which drawer it's physically in
source supplier / order it came from
listed blank until published, then the eBay listing id
notes free text

inventory.csv, committed, hand-edited. One script reconciles it against GET /sell/inventory/v1/inventory_item and prints the three differences that matter: on hand but never listed, listed but zero on hand, quantity mismatch. Nothing writes to eBay from the CSV without an explicit push.

Decisions

  • No third-party inventory manager. Sellbrite, Linnworks, Veeqo, Zoho are all paid SaaS; the free self-hosted ERPs (Odoo, ERPNext, Dolibarr) have weak or commercial-only eBay modules. At dozens of SKUs the eBay API plus a CSV wins.
  • Auth stays in bash. eBay's own ebay-oauth-python-client is the credible library but it is Python, last commit May 2024, and this repo is pure bash with no venv. The refresh-token exchange is one curl call; adding a Python runtime to save 30 lines is the wrong trade.
  • Rejected ldraney-ebay-sdk and navig-ebay (both PyPI, both effectively single-release packages from unknown authors). A one-release package with API credentials passing through it is a supply-chain risk, not a shortcut.
  • state.json, aspects.json, categories.json are gitignored. They are API responses; regenerate them rather than committing stale copies.

Gotchas

  • The Sell APIs reject legacy Auth'n'Auth tokens (AgAAAA...). The OAuth user token starts v^1.1#. check-user-token.sh distinguishes them.
  • An offer cannot publish until the merchant inventory location exists.
  • Sandbox and production keysets are separate. A production Cert ID with a sandbox App ID fails as invalid_client, which reads like a typo.
  • Loose lithium cells cannot ship by air, and every USPS air service (Priority, Priority Express) moves that way. Surface codes differ by environment: production has USPSGroundAdvantage, sandbox's catalogue predates it and offers USPSParcel, which is Parcel Select Ground. USPSGround exists but eBay marks it ValidForSellingFlow=false. Get the real list from the Trading API, GeteBayDetails with DetailName=ShippingServiceDetails; the Sell APIs do not expose it.
  • A fulfillment policy PUT that omits categoryTypes[].default fails with "problem changing the default status of this policy". Send default: false explicitly to match what is already there.
  • A no-op policy PUT returns 400 with "Business Profile information in the request is the same as in the system". That is success, not an error.
  • Updating a policy that a published listing references makes eBay clone the old one as " Copy" with a fresh id. The live offer keeps pointing at the original id, so the change does take effect, but the clone is left behind carrying the old terms.
  • Sandbox will not delete a fulfillment policy. DELETE /sell/account/v1/fulfillment_policy/<id> answers HTTP 500 errorId 20500 System error every time, even with nothing referencing it. A clone can only be neutralised in place, not removed.
  • eBay dedupes fulfillment policies by their settings, not their name. A PUT whose body matches another policy fails with DuplicateProfileName and names the id it collides with, so two policies cannot be made identical.
  • Both halves have offline self-checks that need no eBay credentials: ./auth.sh selftest and reconcile.py --selftest.
  • A production keyset stays disabled until marketplace account deletion notifications are configured, which needs a live public privacy-policy URL.

Open

See ~/Notes/tasks.md — this section is now generated from the task register.

Log

  • 2026-09-09: fixed the stray clone 6250043000. Sandbox will not delete a fulfillment policy, so it was renamed "DO NOT USE" and switched to USPSStandardPost. Both policies on the account are now ground, and nothing on the account can put a lithium cell on a plane.
  • 2026-09-09: shipping fixed to ground. create-policies.sh rewritten to upsert rather than only create, which is why the wrong service code had survived a re-run. eBay cloned the old policy as "Standard Shipping Copy" 6250043000; still to be deleted.
  • 2026-09-09: auth proved end to end against sandbox. ./auth.sh login with the RuName Embeddedera_LLC-Embedded-Embedd-oacnqwayx, refresh token stored, check-user-token.sh returns HTTP 200 and inventory.sh reconciles clean. The RuName's redirect lands on a 404 at embeddedera.com/ebay-auth/, which is harmless: only the ?code= in the address bar matters.
  • 2026-09-09: moved the working copy into ~/Documents/GitHub with the other repos. Scripts cd $(dirname $0) so nothing broke.
  • 2026-09-09: auth.sh replaces the hand-pasted token everywhere. Refresh token in .env, access token in a gitignored cache, refreshed when it is within 5 minutes of expiry. Untested against eBay pending the RuName.
  • 2026-09-09: inventory built as designed — CSV for cost/bin/on-hand, eBay for price and listed quantity, reconcile.py reports the gaps and writes to neither. Both new scripts carry assert-based offline self-checks.
  • 2026-09-09: sandbox integration built — auth check, policies, location, category and aspect lookup, publish. First sandbox listing 110590622889.
  • 2026-09-09: repo pushed to github.com/angelEmbeddedEra/ebay-store, private. No credential ever reached a commit; .env was gitignored from the start.
  • 2026-09-09: researched inventory tooling. Conclusion: eBay's Inventory API is the ledger, a CSV covers cost and bin, no third-party manager is worth it.