Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

wefunder

Official Python SDK for the Wefunder API. Beta — the API is pre-launch and 0.x releases may include breaking changes.

  • Sync (Wefunder) and asyncio (AsyncWefunder) clients on httpx
  • OAuth 2.0: client_credentials for server-to-server, authorization_code + PKCE for user consent, refresh-token rotation handled for you
  • Automatic retries (rate limits, transient errors) and one-shot token recovery on 401
  • Lazy auto-pagination over opaque cursors
  • Typed errors with the request_id support asks for
  • Webhook signature verification, envelope parsing, and dispatch
  • A generated, typed layer for every public operation, plus a request() escape hatch

Requires Python 3.11+.

Install

pip install --pre wefunder

--pre is needed while the SDK is in beta (PEP 440 pre-releases are not selected by default).

Authentication

Wefunder supports two OAuth grants:

  • Use client_credentials for server-to-server access to public data.
  • Use authorization_code with PKCE when acting on behalf of a user.

Server-to-server

import os
from wefunder import Wefunder

wf = Wefunder.from_client_credentials(
    client_id=os.environ["WEFUNDER_CLIENT_ID"],
    client_secret=os.environ["WEFUNDER_CLIENT_SECRET"],
    scopes=["read:public"],
)
page = wf.offerings.list()

Client-credentials tokens represent the application, not a user. They cannot call user-scoped endpoints such as wf.users.me() or wf.portfolio.get(). The SDK mints a new token automatically when a client-credentials token expires.

User authorization with PKCE

Generate the authorization URL on your server. Store state and the PKCE verifier in the user's session before redirecting them:

import secrets
from wefunder import create_authorization_url, generate_pkce

pkce = generate_pkce()
state = secrets.token_urlsafe(32)
save_oauth_attempt(state=state, code_verifier=pkce.code_verifier)

url = create_authorization_url(
    client_id=client_id,
    redirect_uri=redirect_uri,
    scopes=["read:investments"],
    state=state,
    pkce=pkce,
)

The consent host is chosen from the client_id: pk_test_ ids go to the sandbox, everything else to wefunder.com. On the callback, validate state and exchange the code:

from wefunder import Wefunder, exchange_code

attempt = consume_oauth_attempt(state)
tokens = exchange_code(
    client_id=client_id,
    client_secret=client_secret,  # omit for public clients
    code=code,
    redirect_uri=redirect_uri,
    code_verifier=attempt.code_verifier,
)

wf = Wefunder(tokens=tokens, client_id=client_id, client_secret=client_secret, store=TokenStore())

Refresh tokens

Refresh tokens rotate. When the SDK refreshes an access token it calls store.save(tokens) with the new set before retrying the request. Persist the entire token set each time, and load it yourself when constructing a client after a restart:

class TokenStore:
    def save(self, tokens):  # may be sync or async
        db.save_tokens(tokens.access_token, tokens.refresh_token, tokens.expires_at)

wf = Wefunder(tokens=load_tokens(), client_id=client_id, client_secret=client_secret, store=TokenStore())

Concurrent requests that hit a 401 at the same time share one refresh. If several application instances can use the same OAuth connection, serialize refreshes for that connection yourself.

Calling the API

offerings = wf.offerings.list(sort="newest")
investments = wf.investments.list(company_id="co_example")
portfolio = wf.portfolio.get()

Namespaces: users, offerings, investments, portfolio, campaigns, syndicates, intents, attribution, and webhook_endpoints. Enum-typed query parameters accept plain strings, and datetime parameters accept ISO-8601 strings.

wf.investments is the Investment Delta API. list() without a cursor bootstraps; pass updated_since or the meta.next_cursor you saved from your last page to receive only records that changed since then. next_cursor is always present, even on the final page, so persist it after every sync.

The API base URL is https://api.wefunder.com. Paths are version-free; the SDK sends the API version in the Wefunder-Version request header.

Pagination

# One page, and its cursor.
page = wf.offerings.list(sort="newest")
print(page.data, page.meta.next_cursor)

# Every item, one page fetched at a time.
for offering in wf.offerings.all(sort="most_raised"):
    print(offering.id)

# Everything in one list.
investments = wf.investments.collect()

Cursors are opaque. Pass the value returned by the API without modifying it.

Errors and retries

API failures raise WefunderError:

from wefunder import WefunderError

try:
    wf.syndicates.get("syn_example")
except WefunderError as err:
    print(err.status, err.type, err.message, err.request_id, err.remediation)

The SDK retries idempotent GET requests after transient network errors, 5xx responses, and rate limits (honouring X-RateLimit-Reset). Write requests are not retried automatically, except once after a 401 has been recovered.

Async

from wefunder import AsyncWefunder

async with await AsyncWefunder.from_client_credentials(client_id=..., client_secret=...) as wf:
    async for offering in wf.offerings.all():
        print(offering.id)

AsyncWefunder has the same plumbing (auth, recovery, retries, typed errors, raw, request()) and namespaces for users, offerings, investments, portfolio, and webhook_endpoints. Reach every other operation with await wf.call(op, ...) (see below).

Webhooks

Webhooks deliver platform events (investment.executed, offering.opened, investment.changed, …) to an HTTPS endpoint you register. Every delivery is signed; the SDK verifies the signature, parses the envelope, and hands you an event.

1. Register an endpoint

Endpoints belong to your application and are managed through the live API (scope write:webhooks). The signing secret is returned only on create and rotate, so store it immediately.

endpoint = wf.webhook_endpoints.create(
    url="https://yourapp.com/webhooks/wefunder",  # public HTTPS; localhost and private IPs are rejected
    events=["offering.opened", "investment.executed"],
    mode="live",  # "test" endpoints receive sandbox events
)
save_secret(endpoint.attributes.secret)

wf.webhook_endpoints also provides list, get, update, remove, rotate_secret, reenable, and test.

2. Verify and handle deliveries

Pass the raw request body, the headers, and your secret to construct_event. It raises WebhookSignatureError (with a reason) when a delivery is not authentic.

from wefunder import WebhookSignatureError, construct_event, dispatch_webhook

@app.post("/webhooks/wefunder")
def receive(request):
    try:
        event = construct_event(request.get_data(), request.headers, WEBHOOK_SECRET)
    except WebhookSignatureError as err:
        return err.reason, 400

    dispatch_webhook(event, {
        "investment.executed": lambda e: record_funding(e.data["id"], e.data["amounts"]["committed"]),
        "offering.opened": lambda e: announce(e.data["company"]["name"]),
        "default": lambda e: log.info("unhandled %s", e.event),
    })
    return "", 200

Deliveries are at-least-once and unordered. Deduplicate on event.id, and where a payload carries occurred_at, keep the state from the latest one you have seen. async_dispatch_webhook accepts coroutine handlers.

3. Test your handler

wf.webhook_endpoints.test(endpoint.id) sends a real, signed example event and reports the outcome. To unit-test your handler without the API, sign a fixture yourself:

from wefunder import sign_webhook

header = sign_webhook(body, secret)
# POST `body` to your handler with `Wefunder-Signature: <header>`

Signature scheme

Each delivery carries Wefunder-Signature: t=<unix seconds>,v1=<hex> where v1 is HMAC-SHA256(secret, "<t>.<raw body>"). Requests whose t is more than five minutes from now are rejected (tolerance_seconds adjusts this). During a secret rotation the header carries one v1 per active secret and construct_event accepts either. verify_webhook and check_webhook_signature expose the check without parsing, and construct_event still accepts the retired attribution headers (X-Wefunder-Signature / X-Wefunder-Timestamp).

Generated operations

Typed namespaces cover the common resources. Every operation in the public OpenAPI specification is generated under wefunder._generated.api.<tag>; call one through the client to get the same auth, retries, and error handling:

from wefunder._generated.api.syndicate_members import list_syndicate_members

members = wf.call(list_syndicate_members, syndicate_id="syn_example")

For a path the generated layer does not know yet, wf.request(method, path, query=..., body=..., headers=...) sends a fully-wrapped request and returns the decoded JSON.

Development

python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
ruff check src tests scripts examples && ruff format --check src tests scripts examples
pyright
pytest

pytest tests/e2e -o addopts="" runs against the sandbox when WEFUNDER_CLIENT_ID and WEFUNDER_CLIENT_SECRET are set.

Generated files in src/wefunder/_generated/ come from spec/openapi.yaml and are never edited by hand.

Conformance vectors

conformance/*.json is the cross-language behavioural contract shared with wefunder-node (and the Ruby SDK): signatures, token rotation, pagination, retries, errors. tests/test_conformance.py runs every case against both clients. The files are vendored from Wefunder/wefunder-node at the ref in conformance/PIN; refresh with python scripts/sync_conformance.py [ref]. Never edit a vector to make the shell pass.

Examples

examples/ is the source of truth for the Python snippets on docs.wefunder.com; see examples/README.md. After adding one, run python scripts/build_examples_manifest.py and remove its operationId from examples/coverage-allowlist.json.

Updating the API specification

python scripts/sync_spec.py /path/to/wefunder   # public-tier spec → spec/openapi.yaml
python scripts/generate.py                      # → src/wefunder/_generated
pyright && pytest

Commit the specification and generated client together.

Releasing

Set the version in src/wefunder/_version.py (PEP 440, e.g. 0.1.0b1), rebuild examples_manifest.json, commit, then:

git tag v0.1.0b1
git push --follow-tags

The release workflow verifies the tag, runs the checks, builds, and publishes to PyPI via trusted publishing.

Metadata

Release files for wefunder 0.1.0b0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for wefunder 0.1.0b0
File Size Uploaded
wefunder-0.1.0b0.tar.gz 220.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for wefunder 0.1.0b0
File Interpreter ABI Platform
wefunder-0.1.0b0-py3-none-any.whl Python 3 none any Details

Total release size: 849.3 kB

Release files / wefunder-0.1.0b0.tar.gz

Download URL wefunder-0.1.0b0.tar.gz
Size 220.1 kB
Tags Source
SHA-256 checksum
How to use checksums
be47cec6863a31b72e7787adc85066443ba368837ec13a9c9330215f3a272055
BLAKE2b-256 checksum
How to use checksums
9e3aa2ed56e18e9ac6eba14817ea62505711d5a37707e9f31dab4a1a00ebd919
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / wefunder-0.1.0b0-py3-none-any.whl

Download URL wefunder-0.1.0b0-py3-none-any.whl
Size 629.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ff8bef7c0b2668057653e2c858215f8d6bf3da0539b2d7e27b01531f61d0e3a5
BLAKE2b-256 checksum
How to use checksums
9e650e13200c403ac3f75b228c77cea280cc134eb5edeec66c0781030a662539
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0b0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page