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")
passport = client.create_passport_model(
application=UUID("your-application-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
This typically completes in 15–20 seconds on Mainnet. On subsequent client.get_passport(id) calls, ipfs_cid will be populated. 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)
data = PassportData.create_encrypted(
confidential_paths=["supplier_id"],
enc_algorithm="sha256",
name="Public name",
supplier_id="SECRET-123",
)
# AES-256-GCM (symmetric, reversible)
key = base64.b64encode(os.urandom(32)).decode("ascii")
data = PassportData.create_encrypted(
confidential_paths=["serial_number", "brand.supplier_id"], # dot-notation supported
enc_algorithm="aes-256-gcm",
enc_key=key, # omit to auto-generate; read it back via data.encryption_key
name="Public",
serial_number="SN-CONFIDENTIAL",
brand={"name": "Public", "supplier_id": "SECRET"},
)
print(f"Save this key: {data.encryption_key}")
passport = client.create_passport_model(
application=UUID("..."),
network="StarknetMainnet",
model_number="enc-model",
data=data.model_dump(),
certified_paths=["serial_number"],
)
Encrypted values carry a encrypted:<algorithm>:<payload> prefix. AES-GCM payloads are versioned (version byte 0x01) and use AAD b"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
# Backend-supported operators: eq, contains, gt, lt, gte, lte, ne
# Filter values are strings — convert datetimes with .isoformat()
filters = [
FilterOperator(field="granularity", operator="eq", value="model"),
FilterOperator(field="modelNumber", operator="contains", value="lead"),
]
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 is capped at 100 by the backend)
all_passports = []
cursor = 1
while True:
resp = client.list_passports(current_page=cursor, page_size=100)
all_passports.extend(resp.data)
if len(resp.data) < 100:
break
cursor += 1
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=UUID("..."),
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 keyban_api_client import PassportClient, KeybanAPIError
with PassportClient(api_key="...") as client:
try:
passport = client.get_passport(some_uuid)
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", ...}
KeybanAPIError inherits from requests.HTTPError. Its __str__ yields "HTTP {status} {title}: {detail}" so print(e) is self-explanatory; structured access via .status_code and .detail remains available for programmatic flows.
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. |
get_passport |
(passport_id: UUID) -> Passport |
Any granularity. |
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 with automatic date/datetime → ISO conversion and create_encrypted(...) |
FilterOperator |
{field: str, operator: str, value: str} |
KeybanAPIError |
Exception raised on any non-2xx HTTP response. |
Passport fields
| Field | Type | Notes |
|---|---|---|
id |
UUID |
Passport identifier. |
application |
UUID or nested object |
Application the passport belongs to. Access via passport.application.id. |
network |
str |
e.g. "StarknetMainnet", "StarknetSepolia". |
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 |
Timezone-aware UTC. |
updated_at |
datetime |
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.0
2.0.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.0 steps below.
Quick upgrade
pip install --upgrade keyban-api-client==2.0.0
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("..."),
network="StarknetMainnet",
granularity="model",
modelNumber="my-model",
data={"brand": "Acme"},
certifiedPaths=["brand"],
))
client.close()
# AFTER (2.0.0)
from keyban_api_client import PassportClient, PassportData
client = PassportClient(api_key="...") # base_url defaults to prod
passport = client.create_passport_model(
application=UUID("..."),
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, or pytest test_client.py -m api for an end-to-end smoke test |
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.statusis no longer sent in request bodies. Was injected as"published"in 1.0.0; the backend did not require it.
From 0.0.x
Apply the 0.0.x → 1.0.0 steps first:
- Remove any
statusfield from create/update calls. Passports are alwayspublished. certifiedPaths(renamedcertified_pathsin 2.0.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.
Then apply the 1.0.0 → 2.0.0 changes listed in this document.
Changelog
See CHANGELOG.md for the full history.
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.1.tar.gz.
File metadata
- Download URL: keyban_api_client-2.0.1.tar.gz
- Upload date:
- Size: 18.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e853f2d9a90c46d02d4461ae2a40843b6c9bfa2cde2746227b7c32dff70146eb
|
|
| MD5 |
82e992d6c00559fb4405794c5009b655
|
|
| BLAKE2b-256 |
341ecfa0ccef7c2dec32f4a4536707478ac26c377d6dbcd193a451f3b29eef40
|
File details
Details for the file keyban_api_client-2.0.1-py3-none-any.whl.
File metadata
- Download URL: keyban_api_client-2.0.1-py3-none-any.whl
- Upload date:
- Size: 13.5 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 |
e378c28a9fddb0b14706ebd7ead062f759a62196c7587f1c614b7a35860c4127
|
|
| MD5 |
e33a8c273abe88b61e7c2c8d0529aaa2
|
|
| BLAKE2b-256 |
1485c6aba82c742cf47c2c9f525414246c4a5776832c0014ecdd9c515951ae21
|