Skip to main content

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 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:

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 > 100HTTP 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.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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keyban_api_client-2.0.0.tar.gz (18.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

keyban_api_client-2.0.0-py3-none-any.whl (13.5 kB view details)

Uploaded Python 3

File details

Details for the file keyban_api_client-2.0.0.tar.gz.

File metadata

  • Download URL: keyban_api_client-2.0.0.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

Hashes for keyban_api_client-2.0.0.tar.gz
Algorithm Hash digest
SHA256 ac4154418e5acff985b131d2934c942c30326b87b75a0ae330bf9008174f7063
MD5 744ab7eccb402fa1a7867350ed364d47
BLAKE2b-256 48e850876cbbfea0b35b2c35be290af32f883928d9d9f4f7fe3c1e6f2037ec1a

See more details on using hashes here.

File details

Details for the file keyban_api_client-2.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for keyban_api_client-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 09dc364cdc23eb732560890b0fbe403aced8a0859675cf19bfa858b1c398db94
MD5 3480b20ed47c38f5ce3e31c3ba03cc9b
BLAKE2b-256 e3a50d9c552b1ed683c59531cf83f097a33d35d44ce40b1013f9875d86c6d7bb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page