bioflow-sdk
The official Python SDK for the BioFlow public API — link-in-bio pages, captured contacts, files, analytics and Standard-Webhooks endpoints, with typed errors, cursor pagination, idempotent writes and a retry policy that mirrors the server's own rules.
Sync and async clients, httpx as the only runtime dependency, full type hints (py.typed).
pip install bioflow-sdk
The distribution name and the import name differ. Install
bioflow-sdk, thenimport bioflow_py:# pip install bioflow-sdk from bioflow_py import BioFlowThe PyPI project is
bioflow-sdk; the importable Python package staysbioflow_pythroughout this README and in every code sample below.
Requires Python 3.10+. The API reference lives at
https://getbioflow.com/docs/api/reference; a TypeScript SDK with the same surface is
available as @getbioflow/sdk.
Authentication
Create an API key in BioFlow → Settings → Developers. Keys look like
bf_live_… (full access) or bf_test_… (read-only — every write returns
403 test_key_read_only, which makes them safe to put in CI).
from bioflow_py import BioFlow
# Explicit
client = BioFlow(api_key="bf_live_…")
# Or from the BIOFLOW_API_KEY environment variable
client = BioFlow()
# The key can also travel in the x-api-key header instead of Authorization
client = BioFlow(api_key="bf_live_…", auth_style="x-api-key")
bf_ keys are secrets: keep them server-side, never in a browser bundle or a mobile app.
Quickstart
from bioflow_py import BioFlow
with BioFlow(api_key="bf_live_…") as bioflow:
page = bioflow.pages.create({"title": "Summer drop"})
bioflow.pages.add_block(
page["id"],
{"type": "link", "data": {"label": "Pre-order", "url": "https://example.com/drop"}},
)
result = bioflow.pages.publish(page["id"])
print(result["status"], result["url"])
Responses are plain dictionaries typed as TypedDicts, so page["id"] type-checks while a
field the server adds tomorrow still arrives intact.
Async
Same surface, every call awaitable:
import asyncio
from bioflow_py import AsyncBioFlow
async def main() -> None:
async with AsyncBioFlow(api_key="bf_live_…") as bioflow:
usage = await bioflow.usage.get()
print(usage["meters"][0]["remaining"])
pages = await bioflow.pages.list(limit=50)
async for page in pages:
print(page["slug"])
asyncio.run(main())
Pagination
List endpoints return a CursorPage. Iterate it to walk the whole collection — following
pages are fetched lazily with the same query — or read .data for just the page in hand.
with BioFlow(api_key="bf_live_…") as bioflow:
# Every contact, transparently paged
for contact in bioflow.contacts.list(limit=100):
print(contact["email"])
# One page at a time
page = bioflow.pages.list(limit=25)
print(page.data, page.has_more, page.next_cursor, page.request_id)
if page.has_next_page():
page = page.next_page()
Cursors are opaque and bound to the query that minted them: never move one between endpoints
(the server answers 400 invalid_request with the field code cursor_operation_mismatch).
Error handling
Every failure is an application/problem+json document (RFC 9457) raised as a typed
exception. Branch on err.code — the stable machine value — never on title/detail.
from bioflow_py import (
BioFlow,
APIError,
BadRequestError,
AuthenticationError,
PermissionDeniedError,
NotFoundError,
ConflictError,
RateLimitError,
QuotaExhaustedError,
)
try:
bioflow.pages.publish("pg_123")
except QuotaExhaustedError as err:
print("monthly quota gone; resets at", err.retry_after_ms)
except RateLimitError:
... # burst limit — the SDK already retried this for you
except PermissionDeniedError as err:
if err.code == "feature_not_enabled":
print("this workspace is on Free — upgrade to Creator or Pro")
except APIError as err:
print(err.status, err.code, err.detail, err.request_id)
code |
HTTP | Exception | What it means |
|---|---|---|---|
invalid_request |
400 | BadRequestError |
Malformed request; see err.errors for field pointers |
invalid_api_key |
401 | AuthenticationError |
Missing, revoked or unknown key |
insufficient_scope |
403 | PermissionDeniedError |
The key lacks the scope this operation needs |
feature_not_enabled |
403 | PermissionDeniedError |
The workspace plan does not include the public API |
test_key_read_only |
403 | PermissionDeniedError |
A bf_test_ key attempted a write |
resource_not_found |
404 | NotFoundError |
No such resource in this workspace |
stale_snapshot |
409 | ConflictError |
expected_updated_at is out of date — re-read and retry |
idempotency_in_progress |
409 | ConflictError |
The same key is still executing |
idempotency_key_reused |
422 | UnprocessableEntityError |
Same key, different body or path |
endpoint_verification_failed |
422 | UnprocessableEntityError |
The webhook URL did not answer the test event with a 2xx |
endpoint_limit_reached |
422 | UnprocessableEntityError |
Too many webhook endpoints |
rate_limited |
429 | RateLimitError |
Burst limit; retried automatically |
quota_exhausted |
429 | QuotaExhaustedError |
Monthly quota consumed; never auto-retried |
internal_error |
500 | InternalServerError |
A BioFlow-side failure |
Codes this SDK build has never heard of fall back to the HTTP status family and keep
err.code verbatim, so a new server code never turns into a crash.
Every exception carries err.request_id (also on every successful response as
X-Request-Id) — quote it in support requests.
Idempotency
Every consequential POST automatically carries an Idempotency-Key (sdk_<uuid4>), which
is what makes those calls safe to retry. Supply your own to make a retry across process
restarts safe too:
bioflow.pages.create({"title": "Summer drop"}, idempotency_key="drop-2026-08")
A replayed (ledgered) response is flagged on the raw result:
raw = bioflow.request(
"POST", "/v1/pages", body={"title": "Summer drop"}, idempotency_key="drop-2026-08"
)
print(raw.idempotency_replayed) # True the second time
Pass auto_idempotency_keys=False to the constructor to opt out entirely.
Rate limits, retries and plan requirements
The public API is available on the Creator and Pro plans; Free workspaces get
403 feature_not_enabled. Burst limits are 60 requests/minute (Creator) and 120 (Pro);
the monthly workspace quota resets on the 1st, UTC. GET /v1/usage is free to call and never
consumes quota.
Limiter state is parsed off every response:
raw = bioflow.request("GET", "/v1/usage")
print(raw.rate_limit.limit, raw.rate_limit.remaining, raw.rate_limit.reset_seconds)
Retry policy (defaults: max_retries=2, timeout=30, max_retry_after=60):
429 rate_limited— retried for any method (the server refused it before executing).429 quota_exhausted— never retried;Retry-Afterpoints at the period boundary.408,5xx, connection errors and timeouts — retried forGET, and forPOSTcarrying anIdempotency-Key.PATCHandDELETEare never auto-retried.Retry-Afteris honoured exactly (delay-seconds or HTTP-date); a wait longer thanmax_retry_afterraises instead of sleeping.- Otherwise exponential backoff,
min(500ms · 2^attempt, 8s)with ±25 % jitter.
Webhook verification
BioFlow signs deliveries with Standard Webhooks v1. Verify the raw body before parsing it — any re-serialization breaks the signature. Verification needs no API key.
Flask
from flask import Flask, request
from bioflow_py import verify_webhook, WebhookVerificationError
app = Flask(__name__)
@app.post("/webhooks/bioflow")
def bioflow_webhook():
try:
event = verify_webhook(request.get_data(), request.headers, WEBHOOK_SECRET)
except WebhookVerificationError:
return "", 400
if event["type"] == "contact.created":
...
return "", 204
FastAPI
from fastapi import FastAPI, Request, Response
from bioflow_py import verify_webhook, WebhookVerificationError
app = FastAPI()
@app.post("/webhooks/bioflow")
async def bioflow_webhook(request: Request) -> Response:
try:
event = verify_webhook(await request.body(), request.headers, WEBHOOK_SECRET)
except WebhookVerificationError:
return Response(status_code=400)
match event["type"]:
case "contact.created":
...
case "page.published":
...
case _:
... # new types ship without a major version — always keep a default
return Response(status_code=204)
Known event types are contact.created, page.published, sale.paid, sale.refunded and
endpoint.test — treat that list as an open set. The webhook-id (whmsg_…) is stable
across retries: use it as your dedup key. During a secret rotation BioFlow signs with both the
old and new secret for 24 h and verify_webhook accepts either.
Escape hatch
For endpoints newer than your installed SDK, client.request() runs the same auth, retry,
idempotency and error pipeline:
raw = bioflow.request("GET", "/v1/some-new-endpoint", query={"limit": 5})
print(raw.data, raw.status, raw.request_id, raw.rate_limit)
bioflow_py.OPERATIONS is the registry of everything this build models — it is pinned 1:1
against the published OpenAPI document by a contract test, so it can never drift.
Configuration reference
BioFlow(
api_key=None, # falls back to $BIOFLOW_API_KEY
base_url="https://app.getbioflow.com",
timeout=30.0,
max_retries=2,
max_retry_after=60.0,
auth_style="bearer", # or "x-api-key"
auto_idempotency_keys=True,
default_headers=None,
debug=False, # True -> stderr, or pass a callable sink
http_client=None, # bring your own httpx.Client
)
Every resource method also accepts timeout=, max_retries=, idempotency_key=, headers=
and extra_query= for a single call. Debug output is secret-redacted: an API key or webhook
secret never reaches a log line.
Links
License
MIT © Devino Solutions Inc.
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 bioflow_sdk-0.1.0.tar.gz.
File metadata
- Download URL: bioflow_sdk-0.1.0.tar.gz
- Upload date:
- Size: 59.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c2d4408684cbb5e6352a21410e0a778f730229cf1e91efd4fdc1e756ecba237
|
|
| MD5 |
c65dd521fb7be2dfe29515f061e86388
|
|
| BLAKE2b-256 |
99a93772d3603cf613709a68ba9fb095d9fe7fdd9df1d51cb861d4a994b2e920
|
Provenance
The following attestation bundles were made for bioflow_sdk-0.1.0.tar.gz:
Publisher:
publish.yml on DevinoSolutions/bioflow-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bioflow_sdk-0.1.0.tar.gz -
Subject digest:
5c2d4408684cbb5e6352a21410e0a778f730229cf1e91efd4fdc1e756ecba237 - Sigstore transparency entry: 2361522723
- Sigstore integration time:
-
Permalink:
DevinoSolutions/bioflow-python@dff4f768cf658be9427234805e44812b641bd4d8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/DevinoSolutions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dff4f768cf658be9427234805e44812b641bd4d8 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file bioflow_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: bioflow_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d9d65b419090d20a4b55e3071c562f5f898a5fa69bba44d618d28b7d1b0e4c9
|
|
| MD5 |
23fe86e786e9aebd82efd5ff4ebe94db
|
|
| BLAKE2b-256 |
60e60979f14870b92b6cdd8f4144e29355f2d7ce68a5853333b1d1f23b6674df
|
Provenance
The following attestation bundles were made for bioflow_sdk-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on DevinoSolutions/bioflow-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
bioflow_sdk-0.1.0-py3-none-any.whl -
Subject digest:
8d9d65b419090d20a4b55e3071c562f5f898a5fa69bba44d618d28b7d1b0e4c9 - Sigstore transparency entry: 2361522760
- Sigstore integration time:
-
Permalink:
DevinoSolutions/bioflow-python@dff4f768cf658be9427234805e44812b641bd4d8 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/DevinoSolutions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@dff4f768cf658be9427234805e44812b641bd4d8 -
Trigger Event:
workflow_dispatch
-
Statement type: