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
master→main. 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.shdone and proven live. All scripts pull tokens from it,inventory.csvandreconcile.pyreconcile 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-clientis 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-sdkandnavig-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.jsonare 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 startsv^1.1#.check-user-token.shdistinguishes 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 offersUSPSParcel, which is Parcel Select Ground.USPSGroundexists but eBay marks itValidForSellingFlow=false. Get the real list from the Trading API,GeteBayDetailswithDetailName=ShippingServiceDetails; the Sell APIs do not expose it. - A fulfillment policy PUT that omits
categoryTypes[].defaultfails with "problem changing the default status of this policy". Senddefault: falseexplicitly 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 500errorId 20500 System errorevery 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
DuplicateProfileNameand 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 selftestandreconcile.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 toUSPSStandardPost. 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.shrewritten 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 loginwith the RuNameEmbeddedera_LLC-Embedded-Embedd-oacnqwayx, refresh token stored,check-user-token.shreturns HTTP 200 andinventory.shreconciles 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.shreplaces 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.pyreports 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;
.envwas 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.