hotmart-python is a typed Python SDK for the Hotmart API. Hotmart is a Brazilian digital products platform for selling courses, ebooks, subscriptions, and memberships — supporting payments via PIX, boleto, credit/debit card, and PayPal.
This SDK handles OAuth, token refresh, retries, rate limits, and pagination automatically. You write business logic; the SDK handles the API.
Documentação em Português disponível em README-ptBR.md.
v1.0 — complete rewrite. After two years without updates, the library was redesigned from scratch: resource-based API,
httpx, Pydantic v2, automatic retries, strict typing, and much more. This is a strong breaking change from v0.x — see the migration guide to update your code. If you're starting fresh, you can ignore this notice.
- Fully typed responses — every API response is a Pydantic v2 model. Your IDE completes field names; no raw dicts, no guessing.
- Autopaginate iterators — every paginated endpoint ships a
*_autopaginatevariant that transparently walks all pages. Oneforloop, all records. - Automatic token management — OAuth token is acquired, cached, and proactively refreshed 5 minutes before expiry. Thread-safe with double-checked locking.
- Retry with exponential backoff — transient errors (5xx, 429) are retried automatically with jitter and
RateLimit-Resetawareness. Configurable viamax_retries. - Proactive rate limit tracking — monitors remaining requests per window and backs off before hitting the limit.
- Clean exception hierarchy — catch only what you care about:
AuthenticationError,RateLimitError,NotFoundError,BadRequestError, and more. - httpx under the hood — persistent connection pool, configurable timeouts, context manager support.
- Forward-compatible kwargs — extra
**kwargsare passed directly as query params, so you can use undocumented or newly added Hotmart parameters without waiting for an SDK update.
- One object, all resources. Instantiate
Hotmartonce and access every resource group as an attribute:client.sales,client.subscriptions,client.products, etc. - Fail loudly. Errors are typed exceptions, never silently swallowed or buried in return values.
- No boilerplate. Authentication, pagination, retries, and connection management are invisible by default. Opt in to configuration only when you need it.
- Strict typing.
mypy --strictpasses. All public APIs are fully annotated. Models useextra="allow"so new API fields don't break your code.
- Installation
- Quick Start
- Authentication
- Resources
- Pagination
- Sandbox Mode
- Error Handling
- Logging
- Context Manager
- Extra Parameters (kwargs)
- Documentation
- Contributing
- License
pip install hotmart-pythonOr with uv:
uv add hotmart-pythonRequirements: Python 3.11+
from hotmart import Hotmart
client = Hotmart(
client_id="your_client_id",
client_secret="your_client_secret",
basic="Basic your_base64_credentials",
)
# Single page
page = client.sales.history(buyer_name="Paula")
for sale in page.items:
print(sale.purchase.transaction, sale.buyer.email)
# All pages — one iterator, no manual pagination
for sale in client.sales.history_autopaginate(transaction_status="APPROVED"):
print(sale.purchase.transaction)Hotmart uses OAuth 2.0 Client Credentials. The SDK handles token acquisition and refresh automatically — you only need to supply three values at startup.
- Log in to Hotmart.
- Go to Tools → Developer Tools → Credentials.
- Generate a new credential set. You will receive:
client_id— your application client IDclient_secret— your application client secretbasic— the Base64-encodedclient_id:client_secretstring prefixed withBasic(Hotmart shows this value directly in the dashboard)
from hotmart import Hotmart
client = Hotmart(
client_id="abcdef12-1234-5678-abcd-abcdef123456",
client_secret="your_secret_here",
basic="Basic YWJjZGVmMTItMTIzNC01Njc4LWFiY2QtYWJjZGVmMTIzNDU2OnlvdXJfc2VjcmV0X2hlcmU=",
)Tokens are valid for 24 hours. The SDK caches the token and proactively refreshes it 5 minutes before expiry using double-checked locking, so concurrent requests never race on token renewal.
# Single page
page = client.sales.history(buyer_name="Paula", transaction_status="APPROVED")
page = client.sales.summary(start_date=1700000000000, end_date=1710000000000)
page = client.sales.participants(buyer_email="paula@example.com")
page = client.sales.commissions(commission_as="PRODUCER")
page = client.sales.price_details(product_id=1234567)
# Refund a transaction
client.sales.refund("HP17715690036014")
# Autopaginate — iterates all pages automatically
for sale in client.sales.history_autopaginate(buyer_name="Paula"):
print(sale.purchase.transaction)| Method | Description |
|---|---|
history(**kwargs) |
List all sales with detailed information |
history_autopaginate(**kwargs) |
Iterator over all pages |
summary(**kwargs) |
Total commission values per currency |
summary_autopaginate(**kwargs) |
Iterator over all pages |
participants(**kwargs) |
Sales user/participant data |
participants_autopaginate(**kwargs) |
Iterator over all pages |
commissions(**kwargs) |
Commission breakdown per sale |
commissions_autopaginate(**kwargs) |
Iterator over all pages |
price_details(**kwargs) |
Price and fee details per sale |
price_details_autopaginate(**kwargs) |
Iterator over all pages |
refund(transaction_code) |
Request a refund for a transaction |
# List subscribers
page = client.subscriptions.list(status="ACTIVE", product_id=1234567)
# Summary
page = client.subscriptions.summary()
# Purchases and transactions for a single subscriber
purchases = client.subscriptions.purchases("SUB-ABC123")
transactions = client.subscriptions.transactions("SUB-ABC123")
# Cancel one or more subscriptions
result = client.subscriptions.cancel(["SUB-ABC123", "SUB-DEF456"], send_mail=True)
# Reactivate subscriptions (bulk)
result = client.subscriptions.reactivate(["SUB-ABC123"], charge=False)
# Reactivate a single subscription
result = client.subscriptions.reactivate_single("SUB-ABC123", charge=True)
# Change billing due day
client.subscriptions.change_due_day("SUB-ABC123", due_day=15)
# Autopaginate
for sub in client.subscriptions.list_autopaginate(status="ACTIVE"):
print(sub.subscriber_code)| Method | Description |
|---|---|
list(**kwargs) |
List subscriptions with filters |
list_autopaginate(**kwargs) |
Iterator over all pages |
summary(**kwargs) |
Subscription summary |
summary_autopaginate(**kwargs) |
Iterator over all pages |
purchases(subscriber_code) |
Purchase history for a subscriber |
transactions(subscriber_code) |
Transactions for a subscriber |
cancel(subscriber_code, send_mail) |
Cancel one or more subscriptions |
reactivate(subscriber_code, charge) |
Reactivate subscriptions (bulk) |
reactivate_single(subscriber_code, charge) |
Reactivate a single subscription |
change_due_day(subscriber_code, due_day) |
Change the billing due day |
# List products
page = client.products.list(status="ACTIVE")
# Offers for a product
page = client.products.offers("product-ucode-here")
# Plans for a product
page = client.products.plans("product-ucode-here")
# Autopaginate
for product in client.products.list_autopaginate():
print(product.name)| Method | Description |
|---|---|
list(**kwargs) |
List all products |
list_autopaginate(**kwargs) |
Iterator over all pages |
offers(ucode, **kwargs) |
Offers for a product |
offers_autopaginate(ucode, **kwargs) |
Iterator over all pages |
plans(ucode, **kwargs) |
Plans for a product |
plans_autopaginate(ucode, **kwargs) |
Iterator over all pages |
# Create a coupon (10% off) for product 1234567
client.coupons.create("1234567", "SUMMER10", discount=10.0)
# List coupons for a product
page = client.coupons.list("1234567")
# Delete a coupon by ID
client.coupons.delete("coupon-id-here")
# Autopaginate
for coupon in client.coupons.list_autopaginate("1234567"):
print(coupon.code)| Method | Description |
|---|---|
create(product_id, coupon_code, discount) |
Create a discount coupon |
list(product_id, **kwargs) |
List coupons for a product |
list_autopaginate(product_id, **kwargs) |
Iterator over all pages |
delete(coupon_id) |
Delete a coupon |
The Club resource requires a subdomain argument — the subdomain of your Members Area.
# Modules in the members area
modules = client.club.modules("my-course-subdomain")
# Pages within a module
pages = client.club.pages("my-course-subdomain", module_id="module-uuid")
# Students enrolled
students = client.club.students("my-course-subdomain")
# Student progress
progress = client.club.student_progress(
"my-course-subdomain",
student_email="student@example.com",
)| Method | Description |
|---|---|
modules(subdomain, **kwargs) |
List modules in the members area |
pages(subdomain, module_id, **kwargs) |
List pages in a module |
students(subdomain, **kwargs) |
List enrolled students |
student_progress(subdomain, **kwargs) |
Student progress data |
# Get event details
event = client.events.get("event-id-here")
# List tickets for a product
page = client.events.tickets(product_id=1234567)
# Autopaginate
for ticket in client.events.tickets_autopaginate(product_id=1234567):
print(ticket.name)| Method | Description |
|---|---|
get(event_id) |
Get event details |
tickets(product_id, **kwargs) |
List tickets for a product |
tickets_autopaginate(product_id, **kwargs) |
Iterator over all pages |
# Create an installment negotiation for a subscriber
result = client.negotiation.create("SUB-ABC123")| Method | Description |
|---|---|
create(subscriber_code) |
Create an installment negotiation |
The Hotmart API uses cursor-based pagination. Each paginated response contains a page_info object with next_page_token.
Returns a PaginatedResponse[T] with .items and .page_info:
page = client.sales.history(max_results=50)
print(f"Got {len(page.items)} items")
print(f"Next token: {page.page_info.next_page_token}")
# Manually fetch next page
next_page = client.sales.history(page_token=page.page_info.next_page_token)Every paginated method has a matching *_autopaginate variant that handles all page-fetching transparently:
for sale in client.sales.history_autopaginate(buyer_name="Paula"):
print(sale.purchase.transaction)The iterator stops when there are no more pages — no token management, no loop conditions.
If you need to act at the end of each page — flush a batch to a database, save a checkpoint, update a progress bar — use the single-page method and control the loop yourself:
page_token = None
while True:
page = client.sales.history(start_date=1700000000000, page_token=page_token)
for sale in page.items:
process(sale)
save_checkpoint(page_token) # per-page side effect
if not page.page_info or not page.page_info.next_page_token:
break
page_token = page.page_info.next_page_tokenUse *_autopaginate when you only need to iterate all records. Use the manual loop when you need to act between pages.
Use sandbox=True to point all requests at Hotmart's sandbox environment. Sandbox and production credentials are not interchangeable — generate sandbox credentials in the Hotmart dashboard under the same Developer Credentials section, selecting "Sandbox" as the environment.
from hotmart import Hotmart
client = Hotmart(
client_id="your_sandbox_client_id",
client_secret="your_sandbox_client_secret",
basic="Basic your_sandbox_base64_credentials",
sandbox=True,
)Note: Some endpoints behave differently or are not fully supported in the sandbox. See SANDBOX-GUIDE.md and HOTMART-API-BUGS.md for known issues.
All SDK errors inherit from HotmartError. Import and catch only the exceptions you need:
from hotmart import (
Hotmart,
HotmartError,
AuthenticationError,
RateLimitError,
NotFoundError,
BadRequestError,
InternalServerError,
)
try:
page = client.sales.history()
except AuthenticationError:
print("Check your credentials.")
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds.")
except NotFoundError:
print("Resource not found.")
except HotmartError as e:
print(f"API error: {e}")Exception hierarchy:
| Exception | HTTP status | Meaning |
|---|---|---|
AuthenticationError |
401, 403 | Invalid or missing credentials |
BadRequestError |
400 | Invalid parameters |
NotFoundError |
404 | Resource not found |
RateLimitError |
429 | Rate limit exceeded (500 req/min) |
InternalServerError |
500, 502, 503 | Hotmart server error |
APIStatusError |
other | Unexpected HTTP status |
HotmartError |
— | Base class for all SDK errors |
Not all errors are equal — the SDK handles them differently before raising:
| Behavior | Exceptions |
|---|---|
| Retried with exponential backoff — raised only after all retries are exhausted | RateLimitError (429), InternalServerError (500, 502, 503) |
| Triggers token refresh + one automatic retry — never raised for an expired token | AuthenticationError from 401 |
| Raised immediately, no retry | BadRequestError (400), AuthenticationError from 403, NotFoundError (404), APIStatusError |
In practice: a RateLimitError in your except block means Hotmart was still returning 429 after all retries. An AuthenticationError means your credentials are genuinely invalid, not just that a token expired mid-run.
Backoff formula: 0.5s × 2^attempt + jitter (jitter 0–0.5s, cap 30s). For 429, the RateLimit-Reset header is used directly when present. Configure the number of retries via max_retries:
client = Hotmart(..., max_retries=5)Logging is disabled by default. Enable it by passing log_level at construction time:
import logging
from hotmart import Hotmart
client = Hotmart(
client_id="...",
client_secret="...",
basic="Basic ...",
log_level=logging.INFO,
)| Level | What is logged |
|---|---|
logging.DEBUG |
Request URLs, parameters — contains sensitive data, avoid in production |
logging.INFO |
High-level operation summaries |
logging.WARNING |
Warnings and unexpected conditions |
logging.ERROR |
Errors during API interactions |
logging.CRITICAL |
Critical failures |
Tokens and credentials are masked in all log output.
Hotmart supports the context manager protocol for automatic cleanup of the underlying HTTP connection pool:
from hotmart import Hotmart
with Hotmart(
client_id="...",
client_secret="...",
basic="Basic ...",
) as client:
for sale in client.sales.history_autopaginate():
print(sale.purchase.transaction)All resource methods accept **kwargs and forward them directly to the API as query parameters. This lets you use undocumented or recently added Hotmart parameters without waiting for an SDK update:
# Pass any query parameter Hotmart supports, even if not in the method signature
page = client.sales.history(some_new_param="value")| Document | Description |
|---|---|
| README-ptBR.md | Esta documentação em Português |
| MIGRATION.md | Upgrading from v0.x to v1.0 — breaking changes and method mapping |
| CHANGELOG.md | Full version history |
| CONTRIBUTING.md | Development setup, code style, how to add endpoints |
| SANDBOX-GUIDE.md | Sandbox environment usage and known limitations |
| HOTMART-API-BUGS.md | Known Hotmart API bugs found during integration testing |
| HOTMART-API-REFERENCE.md | Complete API reference (agent/LLM-friendly — official docs are a JS SPA) |
Contributions are welcome. See CONTRIBUTING.md for setup, coding style, how to add a new endpoint, and the PR checklist.
Apache License 2.0 — see LICENSE.txt for details.
This package is not affiliated with or officially supported by Hotmart.