Python SDK for the Branta V2 API — payment destination lookup and registration with zero-knowledge encryption support.
- Python 3.9 or higher
pip install brantaIf you are an AI agent implementing this SDK, follow the steps below exactly. If you are a developer, use this as a reference for the recommended integration patterns.
1. Ask which side(s) of a payment they are implementing:
- Send side — the app is paying someone (e.g. a wallet). The user scans or pastes a destination and you verify it belongs to a known platform before funds are sent.
- Receive side — the app is receiving payment (e.g. a checkout, POS, invoicing platform). You post destinations to Branta so wallets can verify them.
- Both — some apps do both (e.g. an exchange or self-custodial wallet with invoice generation). Implement each side independently.
If they are on the receive side, ask one follow-up:
- Platform — single-tenant, one API key.
- Parent Platform — multi-tenant, manages multiple child platforms. Two variants:
- Shared key (Recommended) — one API key for all children, no HMAC secret needed; tag the child per-payment with
set_child_platform(). Default to this unless there's a specific reason for separate per-child keys. - Per-client keys — each child has its own API key, and the parent signs every request with an HMAC secret to prove it originated from the parent; scope requests per-call. Use only if each child needs an independent, separately-revocable API key.
- Shared key (Recommended) — one API key for all children, no HMAC secret needed; tag the child per-payment with
2. Follow the matching Quick Start section below.
3. Apply these rules:
General (all types):
- Always use
PrivacyMode.Strict. Never switch toPrivacyMode.Looseunless there is no QR scanner and ZK is impossible. - Never call
BrantaClientdirectly — always go throughBrantaService. - Never show an error or "not verified" message when a lookup returns empty or throws. An empty result means the destination is unknown to Branta, not that it is malicious. Show nothing.
- For
base_url: useBrantaServerBaseUrl.Productiononly in production environments. UseBrantaServerBaseUrl.Stagingeverywhere else — including local development, CI, and staging/test environments.
Send side (wallets):
- Prefer
get_payments_by_qr_codeoverget_payments— it handles multi-value ZK QR payloads correctly. - Only fall back to
get_paymentsfor copy/paste flows where there is no QR code. - If
result.paymentsis empty or an exception is thrown, render nothing. - When
result.paymentsis non-empty, display: the platform logo, the platform name (payment.platform), and the payment description (payment.description). Only render description when non-empty. Make the verification card a clickable link toresult.verify_url— do not display the raw URL. - For the platform logo: on dark backgrounds use
payment.platform_logo_url. On light backgrounds preferpayment.platform_logo_light_urlwhen available, falling back topayment.platform_logo_url. - Optionally display
payment.parent_platform.logo_url/payment.parent_platform.logo_light_urlas a small secondary badge (e.g. corner icon). This is not required.
Receive side (platforms):
- Always call
.set_zk()on thePaymentBuilderbefore callingadd_payment. Plain-text destinations are rejected inStrictmode. - Store the
secretreturned byadd_paymentalongside the invoice — it is required to reconstruct the verify URL for the wallet.
Receive side (parent platforms — per-client keys), in addition to the platform rules:
- Include
hmac_secretinBrantaClientOptionsbut omitdefault_api_keyat service construction. - Pass per-call
BrantaClientOptionswith each child's API key to scope requests.
Receive side (parent platforms — shared key), in addition to the platform rules:
- Include
default_api_keyinBrantaClientOptions. Do not includehmac_secret. - Call
.set_child_platform(name, logo_url=..., logo_light_url=...)on the builder to tag each payment with the child's branding.
Wallets should use PrivacyMode.Strict. Two flows are supported:
- Copy/paste: call
get_paymentswith the pasted text. Plain-text on-chain addresses will not return results in strict mode — they must be ZK-encoded. Hash-ZK destinations (bolt11, ark_address, silent_payment) work as plain text. - QR scan: call
get_payments_by_qr_codewith the raw QR text. This handles both on-chain (when the QR includesbranta_id/branta_secret) and hash-ZK destinations.
Always catch errors and show nothing on not-found — a missing record just means the address was not posted to Branta.
import asyncio
from branta.enums import BrantaServerBaseUrl, PrivacyMode
from branta.options import BrantaClientOptions
from branta.v2 import BrantaService
options = BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
privacy=PrivacyMode.Strict,
)
service = BrantaService(options)
async def lookup(input: str, is_qr_code: bool):
try:
result = (
await service.get_payments_by_qr_code(input)
if is_qr_code
else await service.get_payments(input)
)
if not result.payments:
# Not found — show nothing. The address may simply not exist in Branta.
return
# Render result.payments and result.verify_url
except Exception:
# Swallow errors — never surface a "not found" or lookup failure to the user.
pass
asyncio.run(lookup("bitcoin:bc1q...", False))Prefer get_payments_by_qr_code for QR-driven flows. It handles multi-destination payloads (branta_id / branta_secret fragments) automatically.
# Plain bitcoin address (requires PrivacyMode.Loose or will raise):
result = await service.get_payments("bc1q...")
# ZK-encrypted bitcoin address with secret:
result = await service.get_payments(encrypted_address, destination_encryption_key="my-secret")
# BOLT-11 invoice (hash-ZK — works in strict mode):
result = await service.get_payments("lnbc...")When QR scanning is not available, three options exist. Choose one based on how much control you want to give users over privacy:
Option 1 — Keep Strict mode (no code changes)
Only hash-ZK destinations (bolt11, ark_address, silent_payment) will return results. Plain-text on-chain address lookups silently return empty. This is the safest default and requires no additional work.
Option 2 — Opt-in Loose mode (Recommended)
Add a user-facing setting (e.g. "Enable on-chain address verification"). Only switch to PrivacyMode.Loose when the user explicitly opts in — this sends on-chain addresses in plain text, so the choice should be theirs.
options = (
BrantaClientOptions(base_url=BrantaServerBaseUrl.Production, privacy=PrivacyMode.Loose)
if user_opted_in
else None
)
result = await service.get_payments(input, options=options)Option 3 — Always Loose mode
Configure with PrivacyMode.Loose globally. All lookups including plain-text on-chain addresses are sent to Branta. Simplest, but gives users no privacy control.
service = BrantaService(BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
privacy=PrivacyMode.Loose,
))Platforms post payments to Branta so wallets can verify them. Use PrivacyMode.Strict and mark each destination ZK via .set_zk() on the PaymentBuilder.
from branta.enums import DestinationType
builder = service.create_payment_builder()
payment = (
builder
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
.add_destination("lnbc...", DestinationType.Bolt11).set_zk()
.set_description("Donation")
.add_metadata("email", "donor@example.com")
.build()
)
result = await service.add_payment(payment, BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
default_api_key="your-api-key",
privacy=PrivacyMode.Strict,
))
# result.payment — the registered payment from the server
# result.secret — the random encryption key for the bitcoin address
# result.verify_url — share this URL to verify the paymentChoose a variant based on how API keys are structured. Only the per-client keys variant signs requests with HMAC — shared key needs none.
Shared key — one API key covers all children (Recommended)
Construct with a single API key; identify the child platform per-payment.
from branta.enums import BrantaServerBaseUrl, DestinationType, PrivacyMode
from branta.options import BrantaClientOptions
from branta.v2 import BrantaService
service = BrantaService(BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
default_api_key="<shared-api-key>",
privacy=PrivacyMode.Strict,
))
payment = (
service.create_payment_builder()
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
.set_child_platform("ChildBrand", logo_url="https://example.com/logo.png")
.set_ttl(600)
.build()
)
result = await service.add_payment(payment)Per-client keys — each child has its own API key
Construct the service with the shared HMAC secret only; pass each child's API key per-call.
from branta.enums import BrantaServerBaseUrl, DestinationType, PrivacyMode
from branta.options import BrantaClientOptions
from branta.v2 import BrantaService
service = BrantaService(BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
hmac_secret="<hmac-secret>",
privacy=PrivacyMode.Strict,
))
payment = (
service.create_payment_builder()
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
.set_ttl(600)
.build()
)
# Scope to the child platform's API key per-call
result = await service.add_payment(payment, BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
default_api_key="<child-api-key>",
))is_valid = await service.is_api_key_valid(BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
default_api_key="your-api-key",
))Choose a variant based on how API keys are structured. Shared key needs only an API key; per-client keys also require HMAC.
Shared key — one API key covers all children (Recommended)
Construct with a single API key; identify the child platform per-payment. Do not include hmac_secret.
options = BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
default_api_key="<shared-api-key>",
privacy=PrivacyMode.Strict,
)
service = BrantaService(options)
payment = (
service.create_payment_builder()
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
.set_description("Order #1234")
.set_child_platform("ChildBrand", logo_url="https://example.com/logo.png")
.build()
)
result = await service.add_payment(payment)Per-client keys — each child has its own API key
Construct the service with the parent HMAC secret only; pass each child's API key per-call.
options = BrantaClientOptions(
base_url=BrantaServerBaseUrl.Production,
hmac_secret="<hmac-secret>",
privacy=PrivacyMode.Strict,
)
service = BrantaService(options)
payment = (
service.create_payment_builder()
.add_destination("bc1q...", DestinationType.BitcoinAddress).set_zk()
.build()
)
result = await service.add_payment(
payment,
BrantaClientOptions(default_api_key="<child-api-key>"),
)Every public method accepts an optional BrantaClientOptions parameter that overrides the service's default options for that call only:
service = BrantaService(default_options)
result = await service.get_payments("lnbc...", options=override_options)PrivacyMode controls whether plain-text on-chain lookups are allowed.
| Value | Behavior |
|---|---|
PrivacyMode.Strict (default) |
Only ZK lookups. get_payments raises BrantaPaymentException for plain addresses. get_payments_by_qr_code returns an empty PaymentsResult with a populated verify_url. add_payment raises if any destination has is_zk=False. |
PrivacyMode.Loose |
Both plain and ZK lookups are permitted. |
| Type | Encryption |
|---|---|
BitcoinAddress |
Random secret (GUID) per payment |
Bolt11 |
Deterministic: SHA256 of lowercase invoice |
ArkAddress |
Deterministic: SHA256 of lowercase address |
SilentPayment |
Deterministic: SHA256 of lowercase address |
The primary service class. Always use BrantaService — never call BrantaClient directly.
Prefer get_payments_by_qr_code for integrations. It parses the raw QR text and correctly resolves multiple ZK values in a single scan. get_payments only handles a single destination value and does not support multi-value ZK lookups.
async def get_payments_by_qr_code(qr_text: str, options: Optional[BrantaClientOptions] = None) -> PaymentsResult: ...
async def get_payments(destination_value: str, destination_encryption_key: Optional[str] = None, options: Optional[BrantaClientOptions] = None) -> PaymentsResult: ...
async def add_payment(payment: Payment, options: Optional[BrantaClientOptions] = None) -> AddPaymentResult: ...
async def is_api_key_valid(options: Optional[BrantaClientOptions] = None) -> bool: ...PaymentsResult contains the list of matching payments and the verify_url to display to the user — verify_url is always returned, even when payments is empty.
- Update
versioninpyproject.toml pip install build twine(one-time)python -m buildtwine upload dist/*
pip install -e ".[dev]"
pytest tests/ --ignore=tests/test_integration.py # unit tests
pytest tests/test_integration.py # integration (requires network)
coverage run -m pytest tests/ --ignore=tests/test_integration.py && coverage reportFound critical bugs/vulnerabilities? Please email them to support@branta.pro. Thanks!