BurnLedger Python SDK
Python client for the BurnLedger API — cryptographic deletion certificates for regulatory compliance.
Scope
The SDK covers the data plane: systems, attestations, certificates, webhooks,
API keys, /v1/me, and the transparency log — everything involved in
measuring a datastore and verifying what came back.
Account management (teams, TOTP enrolment, subscriptions, invoices, the audit
log, billing) is deliberately not part of the SDK. Those are administrative
actions people take once, in the dashboard at
https://dashboard.burnledger.io, and they are available on the raw /v1 API
for anyone who needs to automate them.
Install
pip install burnledger
Requirements: Python 3.10+
Quick Start
from burnledger import BurnLedger
with BurnLedger(api_key="dp_...") as dp:
# 1. Attest — snapshot systems before deletion
att = dp.attest("user@example.com", system_ids=["sys_abc", "sys_def"])
# 2. Delete data (your code, your tools)
# 3. Verify — confirm deletion and get certificate
result = dp.verify(att.id, "user@example.com", timeout=60)
# 4. Download certificate PDF
dp.save_pdf(result.certificate.id, "./deletion-cert.pdf")
Async
from burnledger import AsyncBurnLedger
async with AsyncBurnLedger(api_key="dp_...") as dp:
att = await dp.attest("user@example.com", system_ids=["sys_abc"])
result = await dp.verify(att.id, "user@example.com", timeout=60)
Offline Verification
Verify certificates without network access using Ed25519 signatures:
from burnledger import verify_certificate, verify_transparency, PublicKeyInfo
key = PublicKeyInfo.from_hex("abcdef...", revoked=False)
keys = {key.key_id: key}
cert_result = verify_certificate(certificate, keys)
# VerificationResult.VALID or raises VerificationError
log_result = verify_transparency(certificate, keys)
# TransparencyResult.INCLUDED or raises VerificationError
Malformed input — a missing field, a value of the wrong JSON type, bytes that
are neither hex nor base64, a hash or signature of the wrong length — raises
VerificationError too, with a message naming the field
(systems[0].query_hash is 31 bytes, expected 32). A record in a format this
version cannot read raises its subclass UnsupportedFormatVersionError.
Requiring a registration
From format 9.0 every system in a record says whether the enclave checked it
against a registration (authorization: certified, with the customer_key_id
it was registered under) or not (authorization: none). A none record is
still genuine, and verifies by default: whoever relays requests to the enclave
can always leave a registration out, and the enclave cannot refuse that. What
nothing but you can do is refuse the record, so ask for it, naming your key id:
from burnledger import AUTHORIZATION_POLICY_FAILED, AuthorizationPolicy
policy = AuthorizationPolicy(customer_key_ids={"cust_k_..."}) # every system, your key
verify_certificate(certificate, keys, require_authorization=policy)
What a pass means: every required system is certified under a key id you
named, in a record from an enclave image of wire protocol 12 or later — the
first that registers a system only with the signatures of the key group that id
names. So that key group signed the registration. Earlier images, which include
every image that has issued format 9.0 so far, could register a system with no
customer signature, so a certified record from one of them fails the policy,
and so does a record that names no image. The key id is required for the same
reason: certified under a key you did not name could be under a key group the
relay enrolled itself.
systems narrows the requirement to the system ids you name. A record that does
not meet the policy raises VerificationError with
code == AUTHORIZATION_POLICY_FAILED, naming the reason. A record older than
9.0 always fails: nothing signs its authorization field. The CLI takes
--require-certified or --require-certified-system <system_id>, each with
--customer-key-id <id>.
Registering a system with your own key
ADR-025 lets a system's Verification Records say authorization: certified
with your customer_key_id: before measuring it, the enclave checked that the
system's config and query are the ones a key group you hold registered. That
holds for records from an enclave image of protocol 12 or later; the verifiers
say when a record comes from an earlier one, whose certified is not evidence.
All of it happens on your machine, and the SDK never holds or sends a private key.
from burnledger import BurnLedger, generate_key_group
# 1. A key group: n Ed25519 members, any t of which can act.
generated = generate_key_group(members=2, threshold=1)
seeds = [s.export_seed() for s in generated.signers] # store these offline, now
with BurnLedger(api_key="dp_...") as dp:
# 2. Verify the enclave against the PCR0 you pinned out of band. Configs
# are sealed to it, and everything below must come back signed by its key.
enclave = dp.attest_enclave_identity(expected_pcr0=PINNED_PCR0)
# 3. Enrol the group for your team. Every member signs, over the latest the
# authorization may end (`not_after`, by default the enclave's 90-day maximum).
dp.enroll_key_group(
team_id=TEAM_ID, group=generated.group, signers=generated.signers, enclave=enclave
)
# 4. Seal the config to the enclave, create the system with only the
# sealed bytes, and register it, signed at the group's threshold.
result = dp.create_registered_system(
team_id=TEAM_ID,
group=generated.group,
signers=generated.signers[:1],
enclave=enclave,
name="users-db",
connector_type="postgresql",
dsn="postgres://...",
subject_query="SELECT id FROM users WHERE email = $1",
)
# 5. Rotate when you must. The OUTGOING group signs at its threshold and
# every incoming member signs; nothing is re-registered.
successor = generate_key_group(members=2, threshold=1)
dp.rotate_key_group(
team_id=TEAM_ID,
previous_group=generated.group,
previous_signers=generated.signers[:1],
next_group=successor.group,
next_signers=successor.signers,
enclave=enclave,
)
What each call checks. The API relays every document, so each one that
comes back — the enrolment statement, the authorization, the registration — must
verify under the signing key of the enclave you attested, or the call raises
VerificationError. A relay that forged an enrolment would otherwise have you
believe you were registered. The key comes from your attestation, not from
/.well-known/burnledger-keys, which the same API serves. Then the enrolment
must name the group you hold and the team you asked for, its authorization must
end exactly at the not_after you signed and not start in the future, so an
older genuine answer or another team's is refused, and the registration must
name the system, config and connector you signed.
verify_key_enrollment_statement, verify_team_authorization and
verify_system_registration re-check a stored document against the enclave key.
Two of the registration's signed fields are deliberately not compared. Its
query_template_hash is HashQuery over the template after the server
normalizes it, while the request signs sha256 of the raw template, so the two
are not byte-equal even for an honest registration and the SDK has no normalizer
to reconcile them. registered_at is not freshness-checked either, as there is
no signed bound to hold it to. So a relay could hand back an older genuine
registration of this system, under this key and config, over a template you
signed for it earlier; the customer path calls the check right after its own
POST, which narrows that window but does not close it.
A signer is anything with a public_key and a sign(payload) -> bytes method
(KeyGroupSigner), so a member can live in an HSM, a cloud KMS or another
process; InMemorySigner is the convenience for keys on this machine. A system
that already exists is registered with register_system_with_key, given the
exact config bytes it was created with. AsyncBurnLedger has the same methods.
What the records then say. Every Verification Record of a registered system
carries authorization: certified and your customer_key_id for it, and the CLI
prints that key id on the system's Registration line, with what the record's
enclave image makes of it. A system never registered
says authorization: none. A record naming a key id that is not yours means
someone else authorized that verification. The authorization behind your key
expires; enrol the same group again before authorization.not_after to renew it.
Custody. The private keys are yours alone: BurnLedger never sees them and
cannot reissue them. Lose enough members that the group can no longer reach its
threshold and it can neither register a system nor authorize its own rotation,
and there is no recovery, because an operator who could re-enrol on your behalf
is an operator who could mint (ADR-025 §3). The way back is a new group and
re-registering every system, and records before and after will name different key
ids, visibly. So enrol more members than the threshold and keep one offline — two
members, threshold one, is the simplest. A recovery-phrase format is not defined
yet; until it is, back up each member's 32-byte seed (export_seed()) as you
would any credential that cannot be reissued.
Webhook Verification
Verify incoming webhook signatures (HMAC-SHA256):
from burnledger import verify_webhook_signature
valid = verify_webhook_signature(
secret=webhook_secret, # from dp.register_webhook()
body=request.body, # raw request body
signature=request.headers["X-BurnLedger-Signature"],
)
API Reference
Client
BurnLedger(
api_key: str,
*,
base_url: str = "https://api.burnledger.io",
timeout: float = 30.0,
max_retries: int = 2,
)
Systems
| Method | Returns |
|---|---|
register_system(**opts) |
System |
get_system(id) |
System |
list_systems(limit=25) |
SyncPaginator[System] |
deregister_system(id) |
None |
health_check(id) |
System |
Attestations
| Method | Returns |
|---|---|
attest(subject, **opts) |
Attestation |
batch_attest(subjects, **opts) |
BatchAttestationResponse |
get_attestation(id) |
Attestation |
wait_for(id, **opts) |
Attestation |
verify(id, subject, **opts) |
VerifyResult |
Certificates
| Method | Returns |
|---|---|
get_certificate(id) |
CertificateResponse |
list_certificates(limit=25) |
SyncPaginator[CertificateResponse] |
get_certificate_stats() |
CertificateStats |
export_certificates(**opts) |
bytes |
download_pdf(id) |
bytes |
save_pdf(id, path) |
None |
get_revocation_status(id) |
RevocationStatus |
revoke_certificate(id, reason=...) |
CertificateResponse |
batch_revoke_certificates(ids, reason=...) |
BatchRevokeResponse |
batch_revoke_certificates takes up to MAX_BATCH_REVOKE (100) ids under one
reason and returns whether all, some or none were revoked: the failures are in
errors, each naming the request index and certificate_id, so a partially
failed batch is inspected, not caught. More than 100 ids raises ValueError
before any request is made; chunk larger sets by MAX_BATCH_REVOKE.
Webhooks
| Method | Returns |
|---|---|
register_webhook(url=...) |
Webhook |
list_webhooks(limit=25) |
SyncPaginator[Webhook] |
delete_webhook(id) |
None |
rotate_webhook_secret(id) |
WebhookRotateResponse |
commit_webhook_rotation(id) |
None |
list_failed_deliveries(limit=25) |
SyncPaginator[FailedDelivery] |
retry_delivery(delivery_id) |
None |
resolve_delivery(delivery_id) |
None |
API Keys
| Method | Returns |
|---|---|
list_api_keys() |
list[ApiKeyListItem] |
create_api_key(role=..., team_id=None) |
ApiKeyResponse |
revoke_api_key(id) |
None |
Transparency Log
These methods do not require authentication.
| Method | Returns |
|---|---|
get_log_head() |
SignedTreeHead |
get_log_entry(index) |
LogEntry |
get_log_entries(start, end) |
list[LogEntry] |
get_inclusion_proof(index, tree_size) |
InclusionProof |
get_consistency_proof(old_size, new_size) |
ConsistencyProof |
Pagination
All list_* methods return a paginator that auto-fetches pages:
for cert in dp.list_certificates():
print(cert.id)
# async
async for cert in dp.list_certificates():
print(cert.id)
License
MIT
Metadata
Release files for burnledger 0.9.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| burnledger-0.9.1.tar.gz | 181.6 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| burnledger-0.9.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 287.4 kB
Release files / burnledger-0.9.1.tar.gz
| Download URL | burnledger-0.9.1.tar.gz |
|---|---|
| Size | 181.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d6206dc996c0d48ea12c3dde74aa6758fe377e029b92a13a0f89242d10e10458
|
|
BLAKE2b-256 checksum How to use checksums |
8b6e70431b121895c57a6c644803cae42846b1822b1869d66dffaac0fa741d3d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Release files / burnledger-0.9.1-py3-none-any.whl
| Download URL | burnledger-0.9.1-py3-none-any.whl |
|---|---|
| Size | 105.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee4b7fc2e9209dfaba2c2a1ec4293a271dce3f8ff236f494ce4a812ad2efa18f
|
|
BLAKE2b-256 checksum How to use checksums |
1e1efc8b01c031fd8ac2731fee9668d3112f17914e005c1c75d679004c4cf76f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.12.14
|
Security for burnledger 0.9.1
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.