From 2b4b9f631cb0c8fb118f67776441227fc32e2432 Mon Sep 17 00:00:00 2001 From: icepaq Date: Sat, 22 Aug 2026 00:58:00 -0700 Subject: [PATCH] docs: replace the Stainless leftovers in the README with the Fern client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fern owns README.md and rewrites its own sections on every regeneration, but it preserves sections it does not recognize. The Stainless sections survived the cutover that way and now document a client that no longer exists — most dangerously an async example passing `api_key=os.environ.get("WHOP_API_KEY")`, which the Fern client silently ignores and answers with a 401. Drop the eleven stale sections, take Fern's generated text verbatim for the sections it owns, and add hand-written sections for the things Fern cannot know about: the 0.0.41 migration table, the absent env-var fallback, verify_user_token, and the aiohttp extra. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01XV1533iUUxKJxfn4FXptWz --- README.md | 308 +++++++++++++++++++++--------------------------------- 1 file changed, 122 insertions(+), 186 deletions(-) diff --git a/README.md b/README.md index 418533ee..816c370f 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,38 @@ # Whop Python Library -[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=https%3A%2F%2Fgithub.com%2Fwhopio%2Fwhopsdk-python) +[![fern shield](https://img.shields.io/badge/%F0%9F%8C%BF-Built%20with%20Fern-brightgreen)](https://buildwithfern.com?utm_source=github&utm_medium=github&utm_campaign=readme&utm_source=Whop%2FPython) [![pypi](https://img.shields.io/pypi/v/whop_sdk)](https://pypi.python.org/pypi/whop_sdk) -The Whop Python library provides convenient access to the Whop APIs from Python. +The Whop SDK gives you typed access to the Whop API. Pass your API key to the client explicitly — the SDK reads no environment variables, so a client built without a key sends unauthenticated requests and the API answers 401. ## Table of Contents -- [Mcp Server](#mcp-server) - [Documentation](#documentation) - [Installation](#installation) - [Reference](#reference) - [Usage](#usage) -- [Async Usage](#async-usage) -- [Using Types](#using-types) +- [Migrating from 0.0.41 and earlier](#migrating-from-0041-and-earlier) + - [There is no environment-variable fallback](#there-is-no-environment-variable-fallback) +- [A first request](#a-first-request) - [Environments](#environments) - [Async Client](#async-client) +- [Using aiohttp](#using-aiohttp) - [Exception Handling](#exception-handling) - [Pagination](#pagination) -- [Nested Params](#nested-params) -- [Handling Errors](#handling-errors) + - [What a pager exposes](#what-a-pager-exposes) +- [Verifying user tokens](#verifying-user-tokens) - [Advanced](#advanced) - [Access Raw Response Data](#access-raw-response-data) - [Retries](#retries) - [Timeouts](#timeouts) - [Custom Client](#custom-client) -- [Versioning](#versioning) - [Requirements](#requirements) +- [Determining the installed version](#determining-the-installed-version) - [Contributing](#contributing) -## MCP Server - -Use the Whop MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. - -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40whop%2Fmcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkB3aG9wL21jcCJdLCJlbnYiOnsiV0hPUF9BUElfS0VZIjoiTXkgQVBJIEtleSIsIldIT1BfV0VCSE9PS19TRUNSRVQiOiJNeSBXZWJob29rIEtleSIsIldIT1BfQVBQX0lEIjoiYXBwX3h4eHh4eHh4eHh4eHh4IiwiV0hPUF9BUElfVkVSU0lPTiI6IjIwMjYtMDctMDgtMSJ9fQ) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22%40whop%2Fmcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40whop%2Fmcp%22%5D%2C%22env%22%3A%7B%22WHOP_API_KEY%22%3A%22My%20API%20Key%22%2C%22WHOP_WEBHOOK_SECRET%22%3A%22My%20Webhook%20Key%22%2C%22WHOP_APP_ID%22%3A%22app_xxxxxxxxxxxxxx%22%2C%22WHOP_API_VERSION%22%3A%222026-07-08-1%22%7D%7D) - -> Note: You may need to set environment variables in your MCP client. - ## Documentation -The REST API documentation can be found on [docs.whop.com](https://docs.whop.com/apps). The full API of this library can be found in [api.md](api.md). +API reference documentation is available [here](https://docs.whop.com/api-reference). ## Installation @@ -66,75 +58,59 @@ client = Whop( client.access_tokens.create() ``` -## Async usage - -Simply import `AsyncWhop` instead of `Whop` and use `await` with each API call: +## Migrating from 0.0.41 and earlier + +Releases up to and including `0.0.41` were generated by Stainless. `1.0.0` onwards is +generated by Fern, and the constructor is not source-compatible with what came before — +which is why the line left `0.0.x`. + +| Was | Now | +| --- | --- | +| `Whop(api_key=...)` | `Whop(token=...)` — accepts a `str` or a `Callable[[], str]` | +| `Whop(version=...)` | `Whop(api_version_date=...)`, default `"2026-08-21"` | +| `Whop(default_headers={...})` | `Whop(headers={...})` | +| `Whop(http_client=...)` | `Whop(httpx_client=...)` | +| `Whop(webhook_key=...)`, `Whop(app_id=...)` | removed — no constructor equivalent | +| `WHOP_API_KEY` and friends in the environment | removed — see below | +| `client.with_options(max_retries=5).x.y()` | `client.x.y(..., request_options={"max_retries": 5})` | +| `whop_sdk.APIStatusError`, `RateLimitError`, ... | `whop_sdk.core.api_error.ApiError` and the typed subclasses at the package root | +| `model.to_json()` / `model.to_dict()` | Pydantic's `model.model_dump_json()` / `model.model_dump()` | +| `client.webhooks.unwrap(...)` | removed | +| `api.md` | [`reference.md`](https://github.com/whopio/whopsdk-python/blob/HEAD/./reference.md) | + +### There is no environment-variable fallback + +The client reads no environment variables. Setting `WHOP_API_KEY` has no effect, and +`Whop()` with no `token` builds successfully and then sends unauthenticated requests, +so the first sign of the mistake is a `401` from the API rather than an error at +construction time. ```python -import os -import asyncio -from whop_sdk import AsyncWhop +from whop_sdk import Whop -client = AsyncWhop( - api_key=os.environ.get("WHOP_API_KEY"), # This is the default and can be omitted +client = Whop( + token="", + # Optional; both have working defaults. + base_url="https://api.whop.com/api/v1", + api_version_date="2026-08-21", ) - - -async def main() -> None: - page = await client.payments.list( - company_id="biz_xxxxxxxxxxxxxx", - ) - print(page.data) - - -asyncio.run(main()) ``` -Functionality between the synchronous and asynchronous clients is otherwise identical. - -### With aiohttp +Every parameter is keyword-only. -By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend. +## A first request -You can enable this by installing `aiohttp`: - -```sh -# install from PyPI -pip install whop-sdk[aiohttp] -``` - -Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`: +`products.list` is paginated and requires the account to list products for. ```python -import os -import asyncio -from whop_sdk import DefaultAioHttpClient -from whop_sdk import AsyncWhop - - -async def main() -> None: - async with AsyncWhop( - api_key=os.environ.get("WHOP_API_KEY"), # This is the default and can be omitted - http_client=DefaultAioHttpClient(), - ) as client: - page = await client.payments.list( - company_id="biz_xxxxxxxxxxxxxx", - ) - print(page.data) +from whop_sdk import Whop +client = Whop(token="") -asyncio.run(main()) +for product in client.products.list(account_id="biz_xxxxxxxxxxxxxx"): + print(product.id, product.title) ``` -## Using types - -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: - -- Serializing back into JSON, `model.to_json()` -- Converting to a dictionary, `model.to_dict()` - -Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. - ## Environments This SDK allows you to configure different environments for API requests. @@ -169,6 +145,49 @@ async def main() -> None: asyncio.run(main()) ``` +## Using aiohttp + +`AsyncWhop` uses `httpx` by default. To run it on `aiohttp` instead, install the +`aiohttp` extra and pass `DefaultAioHttpClient` as the `httpx_client`: + +```sh +pip install 'whop-sdk[aiohttp]' +``` + +```python +import asyncio + +from whop_sdk import AsyncWhop, DefaultAioHttpClient + + +async def main() -> None: + client = AsyncWhop( + token="", + httpx_client=DefaultAioHttpClient(), + ) + pager = await client.products.list(account_id="biz_xxxxxxxxxxxxxx") + async for product in pager: + print(product.id, product.title) + + +asyncio.run(main()) +``` + +`DefaultAioHttpClient` is importable without the extra, but raises `RuntimeError` when +constructed. + +Neither `Whop` nor `AsyncWhop` is a context manager and neither exposes a `close()`, so +there is no `with` / `async with` form. To shut the transport down cleanly — otherwise +`aiohttp` warns about an unclosed session at exit — keep a reference to the client you +passed in and close that: + +```python +http_client = DefaultAioHttpClient() +client = AsyncWhop(token="", httpx_client=http_client) +... +await http_client.aclose() +``` + ## Exception Handling When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error @@ -207,117 +226,49 @@ for page in pager.iter_pages(): print(item) ``` -## Nested params - -Nested parameters are dictionaries, typed using `TypedDict`, for example: - -```python -from whop_sdk import Whop - -client = Whop() - -app = client.apps.create( - company_id="biz_xxxxxxxxxxxxxx", - name="name", - icon={"id": "id"}, -) -print(app.icon) -``` - -## Handling errors +### What a pager exposes -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `whop_sdk.APIConnectionError` is raised. - -When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `whop_sdk.APIStatusError` is raised, containing `status_code` and `response` properties. - -All errors inherit from `whop_sdk.APIError`. +The pager is not the response body. It carries `items` (this page only), `has_next`, +`next_page()`, and `iter_pages()`, and iterating the pager itself walks every page. The +decoded response — `data` and `page_info` — is on `pager.response`: ```python -import whop_sdk from whop_sdk import Whop -client = Whop() +client = Whop(token="") +pager = client.products.list(account_id="biz_xxxxxxxxxxxxxx") -try: - client.payments.list( - company_id="biz_xxxxxxxxxxxxxx", - ) -except whop_sdk.APIConnectionError as e: - print("The server could not be reached") - print(e.__cause__) # an underlying Exception, likely raised within httpx. -except whop_sdk.RateLimitError as e: - print("A 429 status code was received; we should back off a bit.") -except whop_sdk.APIStatusError as e: - print("Another non-200-range status code was received") - print(e.status_code) - print(e.response) +print(pager.response.page_info.has_next_page) +print(pager.response.data) # this page's items, as returned by the API +print(pager.items) # the same items, off the pager ``` -Error codes are as follows: - -| Status Code | Error Type | -| ----------- | -------------------------- | -| 400 | `BadRequestError` | -| 401 | `AuthenticationError` | -| 403 | `PermissionDeniedError` | -| 404 | `NotFoundError` | -| 422 | `UnprocessableEntityError` | -| 429 | `RateLimitError` | -| >=500 | `InternalServerError` | -| N/A | `APIConnectionError` | +`SyncPager` and `AsyncPager` live in `whop_sdk.core.pagination`; they are not exported +from the package root. -### Retries - -Certain errors are automatically retried 2 times by default, with a short exponential backoff. -Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, -429 Rate Limit, and >=500 Internal errors are all retried by default. - -You can use the `max_retries` option to configure or disable retry settings: - -```python -from whop_sdk import Whop +## Verifying user tokens -# Configure the default for all requests: -client = Whop( - # default is 2 - max_retries=0, -) +`verify_user_token` checks the `x-whop-user-token` JWT that Whop sends to an embedded +app. It is hand-written rather than generated, and it is the only part of this package +that needs a dependency the package does not declare — install `pyjwt` yourself: -# Or, configure per-request: -client.with_options(max_retries=5).payments.list( - company_id="biz_xxxxxxxxxxxxxx", -) +```sh +pip install pyjwt ``` -### Timeouts - -By default requests time out after 1 minute. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: - ```python -from whop_sdk import Whop - -# Configure the default for all requests: -client = Whop( - # 20 seconds (default is 1 minute) - timeout=20.0, -) - -# More granular control: -client = Whop( - timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), -) +from whop_sdk.lib.verify_user_token import verify_user_token -# Override per-request: -client.with_options(timeout=5.0).payments.list( - company_id="biz_xxxxxxxxxxxxxx", -) +payload = verify_user_token(request.headers, app_id="app_xxxxxxxxxxxxxx") +print(payload.user_id) ``` -On timeout, an `APITimeoutError` is thrown. +It accepts either the raw token or a headers mapping, and takes optional `public_key`, +`jwks_url`, and `header_name` overrides. By default it fetches Whop's public signing +keys from `https://api.whop.com/.well-known/jwks.json` and caches them in-process. -Note that requests that time out are [retried twice by default](#retries). +> The module docstring suggests `pip install 'whop-sdk[user-tokens]'`. That extra does +> not exist on the published distribution; `aiohttp` is the only one. ## Advanced @@ -399,33 +350,18 @@ client = Whop( ) ``` -## Versioning - -This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: - -1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ -3. Changes that we do not expect to impact the vast majority of users in practice. - -We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. - -We are keen for your feedback; please open an [issue](https://www.github.com/whopio/whopsdk-python/issues) with questions, bugs, or suggestions. - -### Determining the installed version +## Requirements -If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. +Python 3.10 or higher. -You can determine the version that is being used at runtime with: +## Determining the installed version -```py +```python import whop_sdk + print(whop_sdk.__version__) ``` -## Requirements - -Python 3.9 or higher. - ## Contributing While we value open-source contributions to this SDK, this library is generated programmatically.