Python client for the Keyban DPP Passport API
Project description
Keyban API Client
Python client for the Keyban DPP Passport API. Create and manage model-granularity passports with on-chain certification on Starknet, field-level encryption, and typed Pydantic models.
Features
- Model passports — create and update passports certified on Starknet
- On-chain certification — automatic W3C Verifiable Credential issuance (P-256
ecdsa-jcs-2019) anchored on Starknet, signed content uploaded to IPFS - Selective certification —
certified_pathsto choose which fields fromdatago into the certificate; updating non-certified fields skips re-certification - Field-level encryption — SHA-256 hashing (integrity) or AES-256-GCM (reversible) via
PassportData - Typed responses — Pydantic v2 models;
py.typedmarker for mypy users
Install
pip install keyban-api-client
Requires Python 3.9+.
Quick start
from uuid import UUID
from keyban_api_client import PassportClient
client = PassportClient(api_key="your-api-key")
# base_url defaults to https://api.prod.keyban.io — pass it explicitly for staging/local:
# PassportClient(api_key="...", base_url="https://api.staging.keyban.io")
app_id = UUID("00000000-0000-0000-0000-000000000000") # ← replace with your application UUID
passport = client.create_passport_model(
application=app_id,
network="StarknetMainnet",
model_number="my-model-001",
data={"brand": "Acme", "gtin": "3760001000001"},
certified_paths=["brand", "gtin"],
)
print(f"Passport ID: {passport.id}")
print(f"Token ID: {passport.token_id}") # derived immediately
print(f"IPFS CID: {passport.ipfs_cid}") # None right after creation — see below
Certification is asynchronous
When data is provided on a model passport, the backend queues a certification job. The HTTP response returns immediately with token_id populated but ipfs_cid still None. The worker then:
- Builds a W3C Verifiable Credential (
credentialSubject= byte-for-byte certified content) - Uploads the signed VC to IPFS → populates
ipfs_cid - Publishes a certification event on Starknet with the CID, a SHA-256 canonical content hash, and the certifier public key
Latency depends on the network: a few seconds on devnet, typically 15–20 seconds on Mainnet. The same asynchrony applies to update_passport_model when the certificate content changes — the response returns the previous CID; the new one appears on a later fetch. If the update does not change the certificate content (e.g. you touched only a field outside certified_paths), the CID stays the same — no re-certification is triggered.
A first-class helper to wait for certification status will land in a future version.
Encryption
Protect sensitive fields locally before sending them to the API:
| Algorithm | Reversible | Use case |
|---|---|---|
sha256 |
No | Integrity / existence proof |
aes-256-gcm |
Yes | Confidential data (decrypt with the key) |
import base64, os
from keyban_api_client import PassportData
# SHA-256 (one-way hash) — encryption_key is NOT set on this path
data = PassportData.create_encrypted(
confidential_paths=["supplier_id"],
enc_algorithm="sha256",
name="Public name",
supplier_id="SECRET-123",
)
# data.model_dump() → {"name": "Public name", "supplier_id": "encrypted:sha256:<hex>"}
# AES-256-GCM (symmetric, reversible). Omit `enc_key` to let the SDK generate one;
# read it back via `data.encryption_key` (only set on the AES path).
data = PassportData.create_encrypted(
confidential_paths=["serial_number", "brand.supplier_id"], # dot-notation supported
enc_algorithm="aes-256-gcm",
name="Public",
serial_number="SN-CONFIDENTIAL",
brand={"name": "Public", "supplier_id": "SECRET"},
)
key_to_persist = data.encryption_key # base64-encoded 32 bytes — store this in your secret manager
# data.model_dump() → {"serial_number": "encrypted:aes-256-gcm:<b64>", ...}
passport = client.create_passport_model(
application=app_id,
network="StarknetMainnet",
model_number="enc-model",
data=data.model_dump(),
certified_paths=["serial_number"],
)
Do not log the AES key. Treat
data.encryption_keylike any other secret — if it leaks into logs, the chiffré can be reversed.
Payload format
Encrypted values carry an encrypted:<algorithm>:<payload> prefix.
- SHA-256 payload is hex of
sha256(canonical_json_value). Irreversible — used for proof of existence/integrity only. - AES-256-GCM payload is
base64(version || nonce || ciphertext || tag)whereversion = 0x01,nonceis 12 bytes, and the AEAD tag (16 bytes) is appended to the ciphertext bycryptography's AESGCM. AAD is the literal byte stringb"v1:aes-256-gcm".
Security: create_encrypted raises ValueError if a confidential_paths entry does not exist in the data. This prevents silently shipping unencrypted secrets on a typo.
Filtering, pagination, listing
from keyban_api_client import FilterOperator
# Filter values are strings — convert datetimes with .isoformat(), UUIDs with str(...)
filters = [
FilterOperator(field="granularity", operator="eq", value="model"),
FilterOperator(field="modelNumber", operator="eq", value="my-model-001"),
]
page = client.list_passports(filters=filters, current_page=1, page_size=50)
for p in page.data:
print(f"- {p.id} {p.granularity} {p.model_number}")
print(f"total matching: {page.total}")
# Paginate. page_size > 100 returns HTTP 400 (not silently capped).
all_passports = []
cursor = 1
while True:
resp = client.list_passports(current_page=cursor, page_size=100)
all_passports.extend(resp.data)
if not resp.data or len(all_passports) >= resp.total:
break
cursor += 1
Today the public passport endpoint only accepts
eqongranularityandmodelNumber. Other field/operator combinations return HTTP 400.fielduses the backend (camelCase) name —modelNumber, notmodel_number.
Selective certification with certified_paths
By default (or with certified_paths=[]) the full data object is certified. Pass specific paths to certify only a subset — updating non-certified fields won't trigger re-certification:
passport = client.create_passport_model(
application=app_id,
network="StarknetMainnet",
model_number="selective",
data={"brand": "Acme", "gtin": "3760001000001", "notes": "internal memo"},
certified_paths=["brand", "gtin"],
)
# Update a non-certified field — NO re-certification
client.update_passport_model(
passport.id,
data={"brand": "Acme", "gtin": "3760001000001", "notes": "updated memo"},
)
# Update a certified field — re-certification triggers; new ipfs_cid after the job runs
client.update_passport_model(
passport.id,
data={"brand": "NewBrand", "gtin": "3760001000001", "notes": "updated memo"},
)
# Change the selection itself — re-certification triggers
client.update_passport_model(passport.id, certified_paths=["brand", "gtin", "notes"])
The backend only re-certifies when the resulting certificate content actually changes (content hash differs).
Error handling
from uuid import UUID
import requests
from keyban_api_client import PassportClient, KeybanAPIError
with PassportClient(api_key="...") as client:
try:
passport = client.get_passport(UUID("00000000-0000-0000-0000-000000000000"))
except KeybanAPIError as e:
print(e) # e.g. "HTTP 404: Not Found"
print(e.status_code) # 404
print(e.detail) # full RFC 7807 body: {"status", "title", "detail", ...}
except requests.RequestException as e:
# DNS failures, connection refused, read timeouts — see note below
print(f"network error: {e}")
KeybanAPIError inherits from requests.HTTPError and is raised on any non-2xx HTTP response. Its __str__ renders as "HTTP {status} {title}: {detail}", with one shortcut: when title == detail (common for 404 Not Found, 401 Unauthorized, …) the message collapses to "HTTP {status}: {title}". Structured access via .status_code and .detail remains available for programmatic flows.
Network-level failures are not wrapped. Connection refused, DNS failures, TLS errors, and read timeouts surface as raw
requests.exceptions.*(they never reach the HTTP layer whereKeybanAPIErroris raised). Catchrequests.RequestExceptionalongsideKeybanAPIErrorif you need a single safety net.
API reference
PassportClient(api_key, base_url="https://api.prod.keyban.io", api_version="v1", timeout=30)
| Method | Signature | Notes |
|---|---|---|
list_passports |
(filters=None, current_page=1, page_size=10) -> PassportListResponse |
Any granularity. page_size > 100 → HTTP 400 from the backend (no client-side check). |
get_passport |
(passport_id: UUID) -> Passport |
Any granularity. The UUID type is advisory — strings are forwarded as-is and only validated server-side. |
create_passport_model |
(*, application, network, model_number, data=None, certified_paths=None) -> Passport |
Granularity is always model. |
update_passport_model |
(passport_id, *, data=None, certified_paths=None) -> Passport |
Re-certification triggers automatically when the certificate content changes. |
close() |
— | Closes the HTTP session. Also supports with PassportClient(...) as client:. |
Public models
| Symbol | Role |
|---|---|
Passport |
Response model for a passport — see full field list below |
PassportListResponse |
{data: List[Passport], total: int} |
PassportData |
Helper to build the data dict — accepts arbitrary keyword arguments (extra='allow'); converts date to ISO strings and exposes create_encrypted(...). Note: datetime values are truncated to date (the time component is dropped) — pre-format with dt.isoformat() if you need full timestamps. |
FilterOperator |
{field: str, operator: str, value: str} — values are forwarded with no client-side validation. |
KeybanAPIError |
Exception raised on any non-2xx HTTP response. Network-level failures are raised as requests.exceptions.* and not wrapped. |
Passport fields
| Field | Type | Notes |
|---|---|---|
id |
UUID |
Passport identifier. |
application |
object with .id: UUID |
Application the passport belongs to. Access via passport.application.id. The concrete class is internal and not part of the public API. |
network |
str |
e.g. "StarknetMainnet", "StarknetSepolia". The SDK forwards the string unchanged; the backend is the source of truth for the accepted set. |
granularity |
str |
"model", "batch", or "item". This SDK only creates "model"; reads are agnostic. |
model_number |
Optional[str] |
Identifier for model granularity. |
batch_number |
Optional[str] |
Identifier for batch granularity. |
item_number |
Optional[str] |
Identifier for item granularity. |
data |
Optional[Dict[str, Any]] |
Arbitrary passport data. |
certified_paths |
Optional[List[str]] |
Dot-notation paths selecting which fields of data go into the certificate. Empty/None ⇒ the entire data is certified. |
token_id |
Optional[str] |
On-chain identifier, populated immediately on create. |
ipfs_cid |
Optional[str] |
IPFS CID of the signed VC. None right after create/update — see the "Certification is asynchronous" section. |
allowed_claim_email |
Optional[str] |
Email allowed to claim this passport (item granularity only). |
created_at |
datetime |
Returned as-is from the backend; expected to be timezone-aware UTC. |
updated_at |
datetime |
Returned as-is from the backend; expected to be timezone-aware UTC. |
API endpoints
All operations go through /v1/dpp/passports:
| Endpoint | Method |
|---|---|
/v1/dpp/passports |
GET, POST |
/v1/dpp/passports/:id |
GET, PATCH |
Migrating to 2.0
2.0 focuses the public surface on passport-model operations. If you are on
0.0.x, apply both the 0.0.x → 1.0.0 steps and the 1.0.0 → 2.0 steps below.
Quick upgrade
pip install --upgrade keyban-api-client
Before / after
# BEFORE (1.0.0)
from keyban_api_client import DppClient, CreatePassportRequest, ProductFields
client = DppClient(
base_url="https://api.prod.keyban.io",
api_key="...",
)
passport = client.create_passport(CreatePassportRequest(
application=UUID("00000000-0000-0000-0000-000000000000"),
network="StarknetMainnet",
granularity="model",
modelNumber="my-model",
data={"brand": "Acme"},
certifiedPaths=["brand"],
))
client.close()
# AFTER (2.0)
from keyban_api_client import PassportClient, PassportData
client = PassportClient(api_key="...") # base_url defaults to prod
passport = client.create_passport_model(
application=UUID("00000000-0000-0000-0000-000000000000"),
network="StarknetMainnet",
model_number="my-model",
data={"brand": "Acme"},
certified_paths=["brand"],
)
client.close()
Renames
| 1.0.0 | 2.0.0 |
|---|---|
DppClient |
PassportClient |
DppPassport |
Passport |
ProductFields |
PassportData |
ProductClient (alias) |
removed — use PassportClient |
Removed (with replacement guidance)
| Removed | Replacement |
|---|---|
client.create_passport(data) |
client.create_passport_model(**kwargs) — kwargs-only; granularity always model |
client.update_passport(id, data) |
client.update_passport_model(id, **kwargs) |
client.delete_passport(id) |
No SDK replacement — model passports anchor on-chain records and are intended to remain addressable |
CreatePassportRequest |
Pass fields directly as keyword arguments to create_passport_model(...) |
UpdatePassportRequest |
Pass fields directly as keyword arguments to update_passport_model(...) |
create_filter(field, op, value) |
FilterOperator(field=..., operator=..., value=...) |
search_passports(client, filters) |
client.list_passports(filters=filters).data |
QueryParams, DynamicFieldDef, Application |
Unused in public flows; made internal or removed |
playground.py |
Use the examples in this README. End-to-end tests (test_client.py -m api) are kept in the source repository, not shipped on PyPI. |
Constructor signature
PassportClient.__init__ swaps parameter order: api_key comes first, base_url is optional and defaults to "https://api.prod.keyban.io".
# 1.0.0 — base_url required and first
DppClient(base_url="...", api_key="...")
# 2.0.0 — api_key first; base_url optional
PassportClient(api_key="...")
PassportClient(api_key="...", base_url="https://api.staging.keyban.io")
Write methods: kwargs-only, snake_case
create_passport_model and update_passport_model take named keyword arguments only. granularity is no longer part of the public surface (it is always "model" for these methods). Camel-case aliases (modelNumber, certifiedPaths) are not accepted — use model_number, certified_paths.
Behavioral changes
FilterOperator.valueis nowstr. Non-string values raise a Pydantic validation error. Convert explicitly:value=str(x),value=dt.isoformat().page_size > 100is no longer silently capped. The backend returnsHTTP 400with a clear error. Usepage_size<=100and paginate.- Missing
confidential_pathsraises.PassportData.create_encrypted(confidential_paths=["typo"], ...)now raisesValueErrorinstead of logging a warning and leaving the data unencrypted — this prevents silent security failures. KeybanAPIError.__str__format changed.print(e)now renders"HTTP 403 Forbidden: Invalid API key."instead of just"Forbidden". Attributese.status_codeande.detailare unchanged. If your code parsesstr(e), switch to the structured attributes.networkis no longer validated client-side. The backend validates; the SDK passes the string through, so new networks work without a client upgrade.
Read access on key passport fields
In 0.0.x, custom data fields lived at the top level of Product/DppPassport (product.name, product.brand, …). On Passport, custom data is nested under data:
# BEFORE (0.0.x)
name = passport.name
# AFTER (2.0)
name = (passport.data or {}).get("name")
Anything not in the closed field list (id, application, network, granularity, model_number, batch_number, item_number, data, certified_paths, token_id, ipfs_cid, allowed_claim_email, created_at, updated_at) is now in passport.data.
From 0.0.x
Apply the 0.0.x → 1.0.0 steps first:
- Move any
status-driven workflow off the request body. Passports are alwayspublishedfrom the user's perspective; the SDK injectsstatus: "published"internally on create where the backend currently requires it. certifiedPaths(renamedcertified_pathsin 2.0) is available again on model passports. Omit or pass an empty list to certify the fulldata.- The old "product" vocabulary (
ProductClient,ProductFields,CreateProductRequest, …) has been fully replaced. Follow the renames table above. - Move custom-field reads from
passport.<field>topassport.data["<field>"](see the section above).
Then apply the 1.0.0 → 2.0 changes listed in this document.
Changelog
The full history is maintained in CHANGELOG.md in the source repository. Highlights:
- 2.0.2 — Documentation pass: filter example aligned with the operators the backend actually accepts (
eqongranularity/modelNumber); AES payload format documented;KeybanAPIError__str__and network-error handling clarified;passport.<field>→passport.data["<field>"]migration note added. - 2.0.1 —
create_passport_modelre-injectsstatus: "published"to match the backend's Draft/Published flow (passports are certifiable immediately on create again). - 2.0.0 — Public surface scoped to model-granularity passports. Renames:
DppClient→PassportClient,ProductFields→PassportData. See the migration section above.
License
MIT — this client is part of the DAP (Digital Asset Platform) by Keyban project.
Project details
Release history Release notifications | RSS feed
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 keyban_api_client-2.0.2.tar.gz.
File metadata
- Download URL: keyban_api_client-2.0.2.tar.gz
- Upload date:
- Size: 20.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f61763f38c68798bcb09921e6fea2c6b7a2dbf835f6ff97c3dda64a2257fb1c
|
|
| MD5 |
a492cf73e84f34c2fb0d25c3e2775975
|
|
| BLAKE2b-256 |
e3f319a08fe3bf4422b93cfbb16010def6db4bf40ba391e1a425ceebbf43df4c
|
File details
Details for the file keyban_api_client-2.0.2-py3-none-any.whl.
File metadata
- Download URL: keyban_api_client-2.0.2-py3-none-any.whl
- Upload date:
- Size: 14.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
57f6aa94857d3fafcde9766f0d4d5eefad8c37f5bda6b24a6a62c9b3dd8c450f
|
|
| MD5 |
f205641e3b3f873ee03f42c889d3abdd
|
|
| BLAKE2b-256 |
db9f9682729944b0cb0639be1a548233c216b1ae0dd9082ae03e50a4ed6e4ed9
|