Skip to main content

A modern, typed Python SDK for the eBay REST APIs (unofficial).

Project description

bidkit

A modern, typed Python SDK for the eBay REST APIs — sync and async, generated from eBay's OpenAPI contracts.

Unofficial. This project is not affiliated with or endorsed by eBay Inc. "eBay" is a trademark of eBay Inc. See NOTICE.

CI PyPI Python License: MIT

Documentation: heyalexej.github.io/bidkit — guides, API reference, and the full generated reference for all 41 APIs.

  • 41 eBay APIs, 455 typed operations across buy, commerce, developer, post_order, and sell namespaces — one client: client.buy.browse.search(q="...")
  • Pydantic v2 models for every request and response, generated from eBay's own OpenAPI contracts; unknown fields and new enum values never break validation
  • Sync and async (EbayClient / AsyncEbayClient) on httpx, with orjson serialization
  • OAuth built in: client-credentials and user tokens, automatic refresh with stampede-proof caching, authorization-code flow helpers
  • Automatic retries for 429/transient 5xx with Retry-After support and full-jitter backoff
  • eBay digital signatures (RFC 9421-style, Ed25519/RSA) for the Finances API
  • Fast imports: lazy-loaded model modules — constructing a client costs tens of milliseconds, and you only pay for the services you use

Requires Python 3.11+.

Installation

uv add bidkit          # or: pip install bidkit

Quickstart

from bidkit import EbayClient, EbayConfig

client = EbayClient(EbayConfig(app_id="...", cert_id="..."))  # or EbayClient.from_env()
results = client.buy.browse.search(q="vintage radio", limit=5)
for item in results.item_summaries or []:
    print(item.title, item.price.value if item.price else "?")

EbayConfig.from_env() reads EBAY_APP_ID, EBAY_CERT_ID, EBAY_REFRESH_TOKEN, EBAY_MARKETPLACE_ID, EBAY_SANDBOX, and friends — see bidkit.config for the full list.

Scope

bidkit covers eBay's REST APIs (Sell, Buy, Commerce, Developer, Post-Order). The legacy XML APIs (Trading, Shopping, Finding) are out of scope by design — they predate OpenAPI and cannot participate in the generated, typed architecture. If you need Trading API calls, the community ebaysdk package (unmaintained since 2020, but functional) can be used alongside.

Authentication (eBay OAuth)

Application-scoped calls need only app_id + cert_id (the client-credentials grant is handled automatically). To act on behalf of a seller you need a user refresh_token. The authorization-code flow is a one-time, three-step exchange:

client = EbayClient(EbayConfig(app_id="...", cert_id="...", ru_name="...", scopes=(...,)))

# 1. Send the user to this URL to grant consent.
print(client.authorization_url(state="..."))

# 2. eBay redirects to your RuName's accepted URL with ?code=<...>; grab that code.
# 3. Exchange it — this also authenticates the client immediately.
tokens = client.exchange_code(code)
print(tokens.refresh_token)  # persist this; pass it back as EbayConfig(refresh_token=...)

Only the redirect capture (step 2) needs the HTTPS "accepted URL" you registered for your RuName in the eBay developer console — the exchange itself is a plain backend call. You can capture the code with your own redirect handler, by copying it from the browser, or via browser automation; the SDK only needs the resulting code. AsyncEbayClient.exchange_code is the async equivalent.

scripts/oauth_login.py runs the whole flow. Interactively it opens the system browser and prompts for the redirect URL; or pass the value as a flag to skip the prompt entirely:

# interactive: opens the browser, then paste the redirect URL at the prompt
uv run --extra dev scripts/oauth_login.py        # reads app/cert/ru/scopes from ebay-cli config

# two-step (no interactive prompt): print the URL, consent, then pass the redirect back
uv run --extra dev scripts/oauth_login.py --no-browser
uv run --extra dev scripts/oauth_login.py --redirect-url 'https://your-ru-url/?code=...'

# persist the result: writes refresh_token + expiries back into --config (other fields kept)
uv run --extra dev scripts/oauth_login.py --redirect-url '...' --write-config

The keyset environment is checked against --sandbox: a production App ID (...-PRD-...) used with --sandbox fails eBay auth with invalid_client, so the script stops early and tells you to drop --sandbox (sandbox needs a separate ...-SBX-... keyset).

Token caching

Access tokens are cached in memory by default, so every new process mints a fresh one. Pass a FileTokenCache to persist tokens across runs (stored 0600 under ~/.cache/bidkit/):

from bidkit import EbayClient, FileTokenCache

client = EbayClient(config, token_cache=FileTokenCache())  # or FileTokenCache(path)

Any object implementing the two-method TokenCache protocol (get/set) works — e.g. a Redis-backed cache for multi-host deployments.

Pagination

paginate (and paginate_async) drive any list endpoint across pages and yield the individual items, following eBay's next URL when present and falling back to limit/offset arithmetic otherwise:

from bidkit import paginate

for payout in paginate(client.sell.finances.get_payouts, limit=50):
    print(payout.payout_id)

# async
async for item in paginate_async(client.sell.inventory.get_inventory_items, limit=100):
    ...

Positional path params and query keywords are forwarded to the method; offset/limit are managed for you. Use max_items=N to cap iteration, and items_field="..." to disambiguate responses that carry more than one array.

Retries & rate limiting

Transient responses are retried automatically. By default 429 Too Many Requests and transient 5xx (500/502/503/504) are retried up to max_retries times with exponential backoff + full jitter, honoring the Retry-After header when eBay sends one. Retries are method-aware: idempotent methods (GET/HEAD/OPTIONS/PUT/DELETE) are replayed on both 429 and 5xx, while non-idempotent POST is replayed only on 429 (the request was rejected before processing). Tune via EbayConfig:

EbayConfig(
    max_retries=2,                       # 0 disables retries
    retry_statuses=(429, 500, 502, 503, 504),
    retry_backoff=0.5,                   # base seconds; delay = backoff * 2**attempt (jittered)
    retry_max_backoff=60.0,
    respect_retry_after=True,
)

For a scoped override, client.with_options(max_retries=0, timeout=5.0) returns a client sharing the same connection pool and token cache with those fields changed.

Call quota

eBay does not send quota headers on responses; remaining quota lives behind two Developer Analytics lookups, which need different token types:

# Application quota: requires an application token (client-credentials, base scope)
app = client.with_options(refresh_token=None, scopes=("https://api.ebay.com/oauth/api_scope",))
for rl in app.developer.analytics.get_rate_limits().rate_limits or []:
    ...

# Per-user quota: requires the user token (a client configured with refresh_token)
for rl in client.developer.analytics.get_user_rate_limits().rate_limits or []:
    ...

Tip: fetch unfiltered and filter client-side — the api_context/api_name server-side filters are case-sensitive and unreliable, and eBay's payload mixes casings ("Sell", "commerce", "TradingAPI").

Logging

bidkit is silent by default and logs through the standard library under the bidkit namespace, so it composes with whatever your application uses (plain logging, structlog, JSON formatters, OpenTelemetry handlers). Opt in per subsystem:

import logging

logging.basicConfig(level=logging.INFO)
logging.getLogger("bidkit").setLevel(logging.DEBUG)   # or just "bidkit.retry"
DEBUG:bidkit.transport:getPayouts GET https://apiz.ebay.com/sell/finances/v1/payout -> 200 (312 ms)
INFO:bidkit.auth:refreshed user token for refresh:1a2b3c4d… (expires in 7200 s)
WARNING:bidkit.retry:getOrders attempt 1/3: HTTP 429, retrying in 1.8 s (Retry-After)

Levels: requests at DEBUG (bidkit.transport), token acquisition at INFO (bidkit.auth), retries at WARNING (bidkit.retry); failures raise exceptions instead of being logged twice. Every record also carries structured fields (operation, method, status, elapsed_ms, attempt, delay_s, …) for JSON/structured formatters. Secrets — tokens, Authorization headers, request bodies — are never logged. For wire-level detail, enable the httpx/httpcore loggers; for tracing, the OpenTelemetry httpx instrumentation works out of the box since bidkit rides on httpx.

Digital signatures (Finances API)

The Finances API and several refund operations (Fulfillment issueRefund, the Post-Order issue-refund calls) reject requests unless they carry an RFC 9421-style HTTP message signature (x-ebay-signature-key + Signature headers). Provide signing material via EbaySigningConfig and exactly those operations are signed automatically — other APIs are left untouched, since eBay does not expect signatures there. If eBay expands the signed list before the SDK catches up, EbaySigningConfig(..., sign_all=True) signs every request:

from bidkit import EbayClient, EbayConfig, EbaySigningConfig

client = EbayClient(EbayConfig(
    refresh_token="...",
    signing=EbaySigningConfig(jwe="<jwe>", private_key="<pem>"),  # or .from_key_file(path)
))
client.sell.finances.get_payouts(limit=3)  # signed; returns 200 instead of 403

The jwe and key come from the Key Management API; Ed25519 (eBay's default) and RSA keys are supported. from_env also reads EBAY_SIGNING_KEY_FILE or EBAY_SIGNING_JWE + EBAY_SIGNING_PRIVATE_KEY.

Verifying eBay push notifications

Production apps must expose a notification endpoint (at minimum for the mandatory marketplace-account-deletion topic). bidkit verifies eBay's signatures and answers the endpoint-validation challenge:

from bidkit import EbayClient, NotificationVerifier, challenge_response

verifier = NotificationVerifier(client)   # app credentials suffice; keys are cached ~1 h

# GET ?challenge_code=...  ->  200, application/json
challenge_response(challenge_code, VERIFICATION_TOKEN, "https://your.app/ebay/notifications")

# POST (notification delivery): verify the RAW body before parsing it
if verifier.verify(raw_body_bytes, request.headers["x-ebay-signature"]):
    ...  # handle, respond 204
else:
    ...  # respond 412; eBay will retry

AsyncNotificationVerifier is the drop-in for async frameworks. verify returns False for bad signatures and raises only on operational failures (key fetch, unsupported key) — respond 500 for those so eBay retries later.

Supported APIs

All 41 eBay APIs below are generated and wired into the client across 5 namespaces (buy, commerce, developer, post_order, sell), exposing 455 typed operations with both sync (EbayClient) and async (AsyncEbayClient) surfaces. Versions are pinned to the bundled OpenAPI specs in specs/ebay/. "Ops" counts callable operations (each also has a raw_response overload and binary downloads add a stream_* variant).

Buy — client.buy (29 ops)

Accessor API Version Ops Maturity
buy.browse Browse v1.20.4 7 Stable
buy.deal Deal v1.3.0 4 Stable
buy.feed Feed (Item Feed) v1_beta.35.3 4 Beta
buy.marketing Marketing 1.1.0 3 Stable
buy.marketplace_insights Marketplace Insights v1_beta.2.0 1 Beta
buy.offer Offer v1_beta.0.1 2 Beta
buy.order Order v2.1.4 8 Stable

Commerce — client.commerce (64 ops)

Accessor API Version Ops Maturity
commerce.catalog Catalog v1_beta.5.3 2 Beta
commerce.charity Charity v1.2.1 2 Stable
commerce.feedback Feedback v1.0.0 5 Stable
commerce.identity Identity v2.0.0 1 Stable
commerce.media Media v1_beta.5.1 13 Beta
commerce.message Message (M2M) 1.0.0 5 Stable
commerce.notification Notification v1.6.7 21 Stable
commerce.taxonomy Taxonomy v1.1.1 9 Stable
commerce.translation Translation v1_beta.1.6 1 Beta
commerce.vero VeRO 1.0.0 5 Stable

Developer — client.developer (6 ops)

Accessor API Version Ops Maturity
developer.analytics Analytics v1_beta.0.1 2 Beta
developer.client_registration Client Registration v1.0.0 1 Stable
developer.key_management Key Management v1.0.0 3 Stable

Post-Order — client.post_order (58 ops)

Accessor API Version Ops Maturity
post_order.cancellation Cancellation v2 * 7 Stable
post_order.case Case Management v2 * 7 Stable
post_order.inquiry Inquiry v2 * 11 Stable
post_order.return_ Return v2 * 33 Stable

* Post-Order specs carry info.version 0.1, but the API is served at /post-order/v2.

Sell — client.sell (298 ops)

Accessor API Version Ops Maturity
sell.account Account v1 v1.9.3 37 Stable
sell.account_v2 Account v2 2.2.0 14 Stable
sell.analytics Analytics 1.3.2 4 Stable
sell.compliance Compliance 1.4.1 3 Stable
sell.edelivery_international_shipping eDelivery Intl Shipping (EDIS) 1.1.0 27 Stable
sell.feed Feed v1.3.1 23 Stable
sell.finances Finances v1.19.0 11 Stable †
sell.fulfillment Fulfillment v1.20.6 15 Stable
sell.inventory Inventory 1.18.5 36 Stable
sell.leads Classified Leads v1.0.0 2 Stable
sell.listing Listing v1_beta.2.1 1 Beta
sell.logistics Logistics v1_beta.0.0 6 Beta
sell.marketing Marketing v1.22.4 80 Stable
sell.metadata Metadata v1.13.0 28 Stable
sell.negotiation Negotiation v1.1.2 2 Stable
sell.recommendation Recommendation v1.1.0 1 Stable
sell.stores Store 1 8 Stable

† Requires a digital signature — see Digital signatures.

Performance

The generated layer is large (40+ model modules), so importing all of it eagerly would make client construction slow. Instead, model modules are lazy-loaded: each <service>_models alias in the generated resources is a proxy that imports its module only when a method of that service is first called. Constructing a client therefore loads no model modules, and you only ever pay for the services you actually use — calling client.buy.browse.* never imports the (much larger) marketing or metadata models.

Generated models also set defer_build=True, so Pydantic compiles a model's validators on its first model_validate(...) rather than at import time. Combined, this takes first client construction from well over a second down to tens of milliseconds, with the remaining per-model cost amortized across first use. Static typing is unaffected — type checkers still resolve the real model types via a TYPE_CHECKING import block.

Development

Raw eBay OpenAPI specs are copied into specs/ebay. The generator preprocesses those specs with explicit eBay compatibility patches, writes normalized specs to specs/normalized (a git-ignored intermediate, never packaged), generates models with datamodel-code-generator, and keeps the HTTP resource surface generated by the local script.

uv run --extra dev scripts/generate_openapi.py   # regenerate clients from specs
uv run --extra dev ruff check .                  # lint
uv run --extra dev ty check src tests            # type check
uv run --extra dev pytest                        # tests

The bundled scripts (scripts/oauth_login.py and the maintainer smoke scripts) read credentials from an ebay-cli style config; see examples/ for templates.

~/.config/ebay-cli/config.json:

Field (credentials.*) Used for Notes
app_id OAuth + all calls aka client_id; the keyset encodes the env (-PRD-/-SBX-)
cert_id OAuth + all calls aka client_secret
ru_name code exchange aka redirect_uri; the registered RuName
refresh_token calling as a seller mint it with oauth_login.py
granted_scopes OAuth + scopes aka scopes; list of scope URLs
dev_id optional

Top-level environment and marketplace_default are convenience hints. ~/.config/ebay-cli/ signing-key.json (jwe + privateKeyPem, optional cipher) feeds the Finances signing layer and maps to EbaySigningConfig.from_key_file(...).

EbayConfig.from_file() loads this format directly (aliases, environment, marketplace_default, and a sibling signing-key.json included):

client = EbayClient(EbayConfig.from_file())   # ~/.config/ebay-cli/config.json

License

MIT — see LICENSE. The eBay OpenAPI contract files under specs/ are © eBay Inc., provided under the eBay API License Agreement, and are not covered by the MIT grant; see NOTICE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

bidkit-0.1.1.tar.gz (556.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

bidkit-0.1.1-py3-none-any.whl (456.1 kB view details)

Uploaded Python 3

File details

Details for the file bidkit-0.1.1.tar.gz.

File metadata

  • Download URL: bidkit-0.1.1.tar.gz
  • Upload date:
  • Size: 556.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bidkit-0.1.1.tar.gz
Algorithm Hash digest
SHA256 509e4f16f7b1b2539d3efaabd4679535e1ad27b9c88486cf79a183cdeeae5b33
MD5 7d300c77df06db2cc75ed034b509b968
BLAKE2b-256 f76cdee8a6e22f9fa23b65fbde039bb7bd8399d7dd6ca098ebb7672bf1199c46

See more details on using hashes here.

Provenance

The following attestation bundles were made for bidkit-0.1.1.tar.gz:

Publisher: publish.yml on heyalexej/bidkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file bidkit-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: bidkit-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 456.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for bidkit-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f1a27313b3267cd553369b6503bdd812f3359661bba7612a4ba8626341e10125
MD5 e6c9873a82bb78045986f7334c8dc3c1
BLAKE2b-256 aae3697fa3b46fb4ea6aec3097f73057a8f87d71b5bf5cb5778ddcc7ff53dbee

See more details on using hashes here.

Provenance

The following attestation bundles were made for bidkit-0.1.1-py3-none-any.whl:

Publisher: publish.yml on heyalexej/bidkit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page