Skip to main content

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 certificationcertified_paths to choose which fields from data go 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.typed marker 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:

  1. Builds a W3C Verifiable Credential (credentialSubject = byte-for-byte certified content)
  2. Uploads the signed VC to IPFS → populates ipfs_cid
  3. 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:

AlgorithmReversibleUse case
sha256NoIntegrity / existence proof
aes-256-gcmYesConfidential 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)

MethodSignatureNotes
list_passports(filters=None, current_page=1, page_size=10) -> PassportListResponseAny granularity. page_size > 100HTTP 400 from the backend.
get_passport(passport_id: UUID) -> PassportAny granularity.
create_passport_model(*, application, network, model_number, data=None, certified_paths=None) -> PassportGranularity is always model.
update_passport_model(passport_id, *, data=None, certified_paths=None) -> PassportRe-certification triggers automatically when the certificate content changes.
close()Closes the HTTP session. Also supports with PassportClient(...) as client:.

Public models

SymbolRole
PassportResponse model for a passport — see full field list below
PassportListResponse\{data: List[Passport], total: int\}
PassportDataHelper to build the data dict with automatic date/datetime → ISO conversion and create_encrypted(...)
FilterOperator\{field: str, operator: str, value: str\}
KeybanAPIErrorException raised on any non-2xx HTTP response.

Passport fields

FieldTypeNotes
idUUIDPassport identifier.
applicationUUID or nested objectApplication the passport belongs to. Access via passport.application.id.
networkstre.g. "StarknetMainnet", "StarknetSepolia".
granularitystr"model", "batch", or "item". This SDK only creates "model"; reads are agnostic.
model_numberOptional[str]Identifier for model granularity.
batch_numberOptional[str]Identifier for batch granularity.
item_numberOptional[str]Identifier for item granularity.
dataOptional[Dict[str, Any]]Arbitrary passport data.
certified_pathsOptional[List[str]]Dot-notation paths selecting which fields of data go into the certificate. Empty/None ⇒ the entire data is certified.
token_idOptional[str]On-chain identifier, populated immediately on create.
ipfs_cidOptional[str]IPFS CID of the signed VC. None right after create/update — see the "Certification is asynchronous" section.
allowed_claim_emailOptional[str]Email allowed to claim this passport (item granularity only).
created_atdatetimeTimezone-aware UTC.
updated_atdatetimeTimezone-aware UTC.

API endpoints

All operations go through /v1/dpp/passports:

EndpointMethod
/v1/dpp/passportsGET, POST
/v1/dpp/passports/:idGET, 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.02.0.0
DppClientPassportClient
DppPassportPassport
ProductFieldsPassportData
ProductClient (alias)removed — use PassportClient

Removed (with replacement guidance)

RemovedReplacement
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
CreatePassportRequestPass fields directly as keyword arguments to create_passport_model(...)
UpdatePassportRequestPass 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, ApplicationUnused in public flows; made internal or removed
playground.pyUse 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.value is now str. Non-string values raise a Pydantic validation error. Convert explicitly: value=str(x), value=dt.isoformat().
  • page_size > 100 is no longer silently capped. The backend returns HTTP 400 with a clear error. Use page_size<=100 and paginate.
  • Missing confidential_paths raises. PassportData.create_encrypted(confidential_paths=["typo"], ...) now raises ValueError instead 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". Attributes e.status_code and e.detail are unchanged. If your code parses str(e), switch to the structured attributes.
  • network is no longer validated client-side. The backend validates; the SDK passes the string through, so new networks work without a client upgrade.
  • status is 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 status field from create/update calls. Passports are always published.
  • certifiedPaths (renamed certified_paths in 2.0.0) is available again on model passports. Omit or pass an empty list to certify the full data.
  • 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.