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")

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:

  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

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_key like 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) where version = 0x01, nonce is 12 bytes, and the AEAD tag (16 bytes) is appended to the ciphertext by cryptography's AESGCM. AAD is the literal byte string 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

# 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 eq on granularity and modelNumber. Other field/operator combinations return HTTP 400. field uses the backend (camelCase) name — modelNumber, not model_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 where KeybanAPIError is raised). Catch requests.RequestException alongside KeybanAPIError if 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 > 100HTTP 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.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.

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 always published from the user's perspective; the SDK injects status: "published" internally on create where the backend currently requires it.
  • certifiedPaths (renamed certified_paths in 2.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.
  • Move custom-field reads from passport.<field> to passport.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 (eq on granularity / modelNumber); AES payload format documented; KeybanAPIError __str__ and network-error handling clarified; passport.<field>passport.data["<field>"] migration note added.
  • 2.0.1create_passport_model re-injects status: "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: DppClientPassportClient, ProductFieldsPassportData. See the migration section above.

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.2.tar.gz (20.9 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.2-py3-none-any.whl (14.9 kB view details)

Uploaded Python 3

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

Hashes for keyban_api_client-2.0.2.tar.gz
Algorithm Hash digest
SHA256 5f61763f38c68798bcb09921e6fea2c6b7a2dbf835f6ff97c3dda64a2257fb1c
MD5 a492cf73e84f34c2fb0d25c3e2775975
BLAKE2b-256 e3f319a08fe3bf4422b93cfbb16010def6db4bf40ba391e1a425ceebbf43df4c

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for keyban_api_client-2.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 57f6aa94857d3fafcde9766f0d4d5eefad8c37f5bda6b24a6a62c9b3dd8c450f
MD5 f205641e3b3f873ee03f42c889d3abdd
BLAKE2b-256 db9f9682729944b0cb0639be1a548233c216b1ae0dd9082ae03e50a4ed6e4ed9

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