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.
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, andsellnamespaces — 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) onhttpx, withorjsonserialization - 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-Aftersupport 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
509e4f16f7b1b2539d3efaabd4679535e1ad27b9c88486cf79a183cdeeae5b33
|
|
| MD5 |
7d300c77df06db2cc75ed034b509b968
|
|
| BLAKE2b-256 |
f76cdee8a6e22f9fa23b65fbde039bb7bd8399d7dd6ca098ebb7672bf1199c46
|
Provenance
The following attestation bundles were made for bidkit-0.1.1.tar.gz:
Publisher:
publish.yml on heyalexej/bidkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bidkit-0.1.1.tar.gz -
Subject digest:
509e4f16f7b1b2539d3efaabd4679535e1ad27b9c88486cf79a183cdeeae5b33 - Sigstore transparency entry: 2047853151
- Sigstore integration time:
-
Permalink:
heyalexej/bidkit@664fb75058249e6a4b7d6eb5c1026d01bee0a052 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/heyalexej
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@664fb75058249e6a4b7d6eb5c1026d01bee0a052 -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1a27313b3267cd553369b6503bdd812f3359661bba7612a4ba8626341e10125
|
|
| MD5 |
e6c9873a82bb78045986f7334c8dc3c1
|
|
| BLAKE2b-256 |
aae3697fa3b46fb4ea6aec3097f73057a8f87d71b5bf5cb5778ddcc7ff53dbee
|
Provenance
The following attestation bundles were made for bidkit-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on heyalexej/bidkit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bidkit-0.1.1-py3-none-any.whl -
Subject digest:
f1a27313b3267cd553369b6503bdd812f3359661bba7612a4ba8626341e10125 - Sigstore transparency entry: 2047853161
- Sigstore integration time:
-
Permalink:
heyalexej/bidkit@664fb75058249e6a4b7d6eb5c1026d01bee0a052 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/heyalexej
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@664fb75058249e6a4b7d6eb5c1026d01bee0a052 -
Trigger Event:
workflow_dispatch
-
Statement type: