Skip to content

About

Self-hosted eBay listing archive & sales manager — an Auctiva replacement. Archive every listing locally, mirror order history, sync live state, leave feedback, relist with scheduled publishing.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Listing Archive

A self-hosted replacement for Auctiva's listing archive and sales manager. One small Flask app + one SQLite file that:

  • archives every listing you've ever created — titles, descriptions, photos, item specifics — so eBay purging old listings never costs you the work again
  • mirrors your order history permanently (eBay's API only retains ~2 years; this keeps everything it can still reach, forever)
  • shows Active / Sold-out / Ended state for the whole archive, synced from eBay every 15 minutes, including Good-Til-Cancelled listings sitting at zero quantity earning nothing
  • relists any archived item in one click — uploads your photos to eBay, fills item specifics, and publishes either immediately or as an eBay Scheduled listing you can review in Seller Hub before it goes live
  • restocks / zeroes quantity on live listings without ending them
  • leaves buyer feedback from a queue of exactly what eBay says you owe
  • prints packing slips and deep-links each order to eBay's label flow
  • handles eBay's marketplace account deletion notifications (the compliance gate for API keys): when a buyer deletes their eBay account, their data is purged from your database automatically and the purge is logged

Everything lives on your hardware. No third party sees your data, your tokens, or your buyers.

Status: extracted from a working single-seller deployment. It runs a real store daily, but expect rough edges — read Hard-won eBay API notes before filing a bug that's actually an eBay behavior.

How it fits together

File Role
app.py Flask UI: archive browser, sales manager, feedback queue, relist pages
build_db.py Auctiva export JSON → archive.db (lossless: raw EAV + flat columns)
fetch_images.py Downloads every listing photo locally (resumable, checksummed)
verify.py 12-point completeness audit — run before cancelling Auctiva
ebay_auth.py OAuth: consent URL, code exchange, token storage, notify-endpoint setup
ebay_client.py Sell-API client (user + application tokens, retries, paging)
ebay_trading.py The few Trading-API (XML) calls with no stable REST equivalent
ebay_sync.py Orders + fulfillments + feedback-awaiting + live-listing sync
ebay_notify.py Account-deletion endpoint, OAuth landing pages, privacy page
ebay_relist.py Media upload → inventory item → offer → publish pipeline
resolve_aspects.py Recovers item-specific names by matching values against eBay Taxonomy
make_fixture.py Synthetic database so tests and demos run without your data
test_*.py Five suites, ~135 checks, all against a faked eBay — nothing real is sent

Quickstart

Requires Python 3.11+ and Flask (pip install flask). No other dependencies.

1. Export your listings from Auctiva (while still subscribed!)

Auctiva has no export feature, but its own catalog UI is fed by a JSON API that returns everything at once. Log into Auctiva in your browser, open the Product Catalog page, open devtools (F12) → Console, and run:

const r = await fetch('/api/Products/Get', {method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({PageIndex: 1, PageSize: 2000,
    FilterCollection: {archived: 0}, AttributeSubset: 2,
    FlattenAndRegroupVariations: false})});
const t = await r.text();
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([t])); a.download = 'auctiva-products-raw.json';
document.body.appendChild(a); a.click();

AttributeSubset: 2 is the key — it returns all ~50 attributes per product including the full description HTML. (0 and 1 return stripped subsets; 3 is a server error.) Also check the archived: 1 view and your standalone Auctiva image library for anything extra.

2. Build and verify the archive

python build_db.py        # auctiva-products-raw.json -> archive.db
python fetch_images.py    # downloads every photo (public CDN, no auth needed)
python verify.py          # 12 checks; cancel Auctiva only on 12/12

Images live at img.auctiva.com/imgdata/<account-digits>/webimg/<id>_o.jpg — _o is the original upload, not a thumbnail. They stop resolving when your subscription lapses, which is why fetch_images.py exists.

3. Run it

python app.py             # http://localhost:8770

Or as a service (systemd):

[Unit]
Description=Listing Archive
After=network-online.target

[Service]
WorkingDirectory=/opt/listing-archive
Environment=BRAND=yourstore
Environment="DEFAULT_FEEDBACK_COMMENT=Great buyer, fast payment. Thank you!"
Environment=MERCHANT_LOCATION_KEY=main-warehouse
Environment="MERCHANT_CITY=Yourtown"
Environment=MERCHANT_STATE=ST
Environment=MERCHANT_POSTAL=00000
Environment=CONTACT_EMAIL=you@example.com
Environment=EBAY_SECRETS=/etc/listing-archive/ebay-api.json
ExecStart=/usr/bin/python3 /opt/listing-archive/app.py
Restart=always
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true

[Install]
WantedBy=multi-user.target

The archive browser works with no eBay credentials at all. Everything below is for the live half (orders, feedback, relisting).

eBay API setup

This is the part where everyone bleeds. In order:

  1. Developer account (developer.ebay.com) — use a business-domain email that is not attached to any eBay account. Free-mail and privacy domains get auto-rejected by a risk filter, and the registration form refuses an email that any eBay account already uses. The developer account is deliberately separate from your seller account; they only meet later, at OAuth consent.
  2. Production keyset — created disabled until you complete the marketplace account deletion requirement.
  3. Account-deletion endpoint — this app ships one (/ebay/account-deletion). Put it on a public HTTPS hostname (a Cloudflare tunnel works well), then: python ebay_auth.py notify (generates the verification token), restart the service, and paste endpoint + token into Application Keys → Notifications. eBay sends a challenge (sha256(code + token + endpoint)) which the endpoint answers; then "Send Test Notification" activates the key.
  4. RuName (User Tokens → Get a Token via Your Application → Add eBay Redirect URL). The app serves /ebay/oauth/accepted, /ebay/oauth/declined and /privacy for the three URLs the form wants. eBay's redirect_uri parameter takes the RuName string, not your URL.
  5. Credentials + consent:
python ebay_auth.py init       # creates the secrets file; fill in app_id/cert_id/ru_name
python ebay_auth.py consent    # prints the consent URL; sign in as YOUR SELLER account
python ebay_auth.py exchange   # paste the ?code= from the redirect (5-min expiry)
python ebay_auth.py test       # mints an access token to prove it works

The Cert ID is on the Application Keys page (~36 chars, PRD-…) — not the "OAuth Application Token" link, which produces a 2000-char JWT you don't want. Scopes requested: sell.fulfillment(+ro), sell.inventory(+ro), sell.account.readonly, commerce.feedback. A refresh token's scopes are fixed at consent — widening them later means consenting again.

  1. Sync:
python ebay_sync.py backfill   # walks order history to eBay's retention floor
python ebay_sync.py update     # incremental; run from cron/systemd-timer every 15 min

Relisting

Every archived listing gets a Relist on eBay page: review title/price/ condition, pick your business policies from live dropdowns, fill any item specifics eBay requires for the category, then:

  • Dry run — sends nothing, shows the exact API payloads
  • Create draft offer — uploads photos, creates an unpublished offer (invisible in Seller Hub — that's an eBay limitation; the app's Relists page is where drafts live)
  • Publish as scheduled — publishes with a future listingStartDate; the listing appears in Seller Hub → Listings → Scheduled, reviewable and deletable in eBay's own UI, and goes live only when the clock hits
  • Publish live now — immediate

Item-specific names come from resolve_aspects.py, which matches your archived values against eBay Taxonomy value lists per category (~94% recovery in the original deployment; the rest are free-text fields the page prompts for).

Configuration

Env var Meaning Default
BRAND Store name shown in UI, packing slips, OAuth pages, privacy page listing archive
ARCHIVE_DB Path to the SQLite database ./archive.db
EBAY_SECRETS Path to the credentials JSON ~/.claude/secrets/ebay-api.json or /root/.ebay-api.json
PORT HTTP port 8770
DEFAULT_FEEDBACK_COMMENT Pre-filled buyer feedback (≤80 chars) generic
MERCHANT_LOCATION_KEY Inventory-location key created on first publish primary-location
MERCHANT_CITY/STATE/POSTAL/COUNTRY Ship-from address (required to publish) unset
CONTACT_EMAIL Shown on the privacy page unset

Multiple sellers = multiple instances (own DB, port, secrets, BRAND) — by design; there is no multi-tenant mode and cross-account isolation is the point.

Tests

python make_fixture.py     # synthetic data; your archive.db is used if present
python test_notify_endpoint.py
python test_sync_safety.py
python test_feedback.py
python test_relist.py
python test_listings.py

All eBay traffic in tests is faked. test_sync_safety.py proves the property the whole sales manager rests on: a resync can never destroy locally-entered notes, folders, or flags.

Hard-won eBay API notes

Facts this codebase paid for, so you don't have to:

  • The Media API lives on apim.ebay.com (not api.ebay.com) and the method is create_image_from_file. The wrong host 404s with an empty body. The image id comes back in the Location response header.
  • createOrReplaceInventoryItem requires Content-Language: en-US and reports its absence as "25709 Invalid value for header Content-Language".
  • GET /sell/inventory/v1/offer?sku=X returns 404 (25713) when the SKU has no offers — not an empty list.
  • publishOffer enforces Brand/MPN ("25002 … <BrandMPN>") in many categories even where Taxonomy marks them merely recommended. MPN: "Does Not Apply" is accepted for items without one.
  • Once published, eBay drops listingStartDate from the offer record — a scheduled listing is indistinguishable from a live one via the Inventory API. Trading GetItem's ListingDetails/StartTime is authoritative.
  • Unpublished API offers never appear in Seller Hub ("Drafts" there is the web form's save state — a different system).
  • GetItemsAwaitingFeedback returns items awaiting feedback in both roles — sales and your purchases (no <Buyer> element). Filter purchases out.
  • The REST commerce/feedback/v1 API returned bare Tomcat 500s on every request shape (Aug 2026). The Trading feedback calls are stable and not on any decommission list.
  • The Taxonomy API needs an application token (client-credentials grant), not the user token — user tokens get 403.
  • ReviseInventoryStatus interprets Quantity inconsistently across listing states (sometimes available, sometimes available+sold) — read back after writing and correct (see ebay_trading.set_available_quantity).
  • eBay retains ended-listing details ~90 days and order history ~2 years. Nothing here can recover what eBay already deleted — archive while your listings (or your Auctiva subscription) still exist.

Privacy & compliance

Your data never leaves your machine except in calls you make to eBay's API. The account-deletion endpoint exists so holding buyer data (orders, addresses) is compliant: deletion notices purge that buyer and log the purge (ebay_deletion_log). If you store nothing (archive-only use), eBay's opt-out path may suit you instead — but don't certify "not persisting eBay data" and then run the order sync.

Disclaimers

Not affiliated with, endorsed by, or supported by eBay Inc. or Auctiva. Trademarks belong to their owners. Export only your own account's data; check the terms of any service you point this at. No warranty — verify (verify.py) before cancelling anything you can't get back.

License

MIT

About

Self-hosted eBay listing archive & sales manager — an Auctiva replacement. Archive every listing locally, mirror order history, sync live state, leave feedback, relist with scheduled publishing.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages