Python client for the Keyban DPP Passport API
Project description
Keyban API Client
Python client for the Keyban DPP Passport API. Create and manage item-granularity passports with on-chain certification on Starknet, field-level encryption, clear-content hash anchoring for trustless verification, and typed Pydantic models. Reads are granularity-agnostic.
Features
- Item passports — create and update passports certified on Starknet; items are the granularity that certifies the free-form
datablob - Clear-content hash anchoring — anchor the hash of the PLAINTEXT
dataon-chain so end users can verify an encrypted passport from the clear certificate file (verify-dpp drag-and-drop) - 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_item(
application=app_id,
network="StarknetMainnet",
item_number="my-item-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 an item passport, the backend queues a certification job. The HTTP response returns immediately with token_id populated, ipfs_cid still None, and certification_status="pending". 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
- Marks the passport as
certification_status="certified"and stampscertified_at
Latency depends on the network: a few seconds on devnet, typically 15–20 seconds on Mainnet. Poll on certification_status to detect the transition ("pending" → "certified", or "error" if all retries are exhausted):
import time
while True:
p = client.get_passport(passport.id)
if p.certification_status == "certified":
break
if p.certification_status == "error":
raise RuntimeError("certification failed")
time.sleep(3)
Re-certification (triggered by an update_passport_item that changes the certified content) flips certification_status back to "pending" until the new on-chain anchor lands; on success, ipfs_cid and certified_at are updated to the new values. If the update does not change the certificate content (e.g. you touched only a field outside certified_paths), the status stays "certified" and the CID is unchanged — no re-certification is triggered.
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_item(
application=app_id,
network="StarknetMainnet",
item_number="enc-item",
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.
Item passports & clear-content hash
Item passports certify their data blob on-chain — use them when each passport
is a unique object (one certificate per lead, one passport per physical unit).
When data fields are encrypted client-side, the backend can only hash the
ciphertext; pass content_hash so the on-chain event anchors the hash of the
plaintext instead, making the certificate verifiable by its holder:
import json
from keyban_api_client import PassportClient, PassportData
pd = PassportData.create_encrypted(
confidential_paths=["first_name", "last_name"],
enc_algorithm="aes-256-gcm",
name="ADO-LEAD-001",
segment="ASSURANCE-EMPRUNT",
first_name="Jane",
last_name="Doe",
)
passport = client.create_passport_item(
application=app_id,
network="StarknetMainnet",
item_number="ADO-LEAD-001",
data=pd.model_dump(), # encrypted payload, stored by Keyban
content_hash=pd.content_hash, # hash of the CLEAR data, anchored on-chain
)
# Hand THIS file to the end user (plus pd.encryption_key, shared separately):
# dropping it in verify-dpp recomputes the same hash and finds the product.
with open("certificate.json", "w") as f:
json.dump(pd.clear_data, f, ensure_ascii=False)
How the hash is computed (compute_content_hash(data)): JCS canonicalization
(RFC 8785) of {"type": "Data", "data": data}, SHA-256, then the first byte is
masked & 0x07 so the value fits a Starknet felt252. Identical byte-for-byte
to the backend and verify-dpp implementations.
Rules to keep verification working:
- Always derive the hash from the data you send — pass
pd.content_hash, never a cached value. A stale hash sent with new data is undetectable server-side and breaks verification for the end user. - Updates must resend the hash:
update_passport_item(id, data=..., content_hash=...)in the SAME call. Updating content without a hash makes the backend fall back to its server-computed (ciphertext) hash. - The hash covers the full clear blob — a
certified_paths-filtered subset will not match what the verify-dpp drop zone recomputes. - JCS constraints:
NaN/Infinityfloats and integers beyond ±(2**53 − 1) raiseValueError(JavaScript cannot canonicalize them either). Use strings for large numeric identifiers.
Filtering, pagination, listing
from keyban_api_client import FilterOperator
# Filter values are strings — convert datetimes with .isoformat(), UUIDs with str(...)
filters = [
FilterOperator(field="product.idGranularity", operator="eq", value="item"),
FilterOperator(field="product.itemNumber", operator="eq", value="my-item-001"),
]
page = client.list_passports(filters=filters, current_page=1, page_size=50)
for p in page.data:
print(f"- {p.id} {p.item_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
The public passport endpoint accepts
eqonapplication.id,status,product.idGranularity,product.modelNumber,product.batchNumber,product.itemNumber,mintedTo.id, andcontainsonproduct.name. Other field/operator combinations return HTTP 400.fielduses the backend (camelCase) path —product.itemNumber, notitem_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_item(
application=app_id,
network="StarknetMainnet",
item_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_item(
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_item(
passport.id,
data={"brand": "NewBrand", "gtin": "3760001000001", "notes": "updated memo"},
)
# Change the selection itself — re-certification triggers
client.update_passport_item(passport.id, certified_paths=["brand", "gtin", "notes"])
The backend only re-certifies when the resulting certificate content actually changes (content hash differs).
Self-hosted (deletable) certificates
By default the signed certificate is uploaded to IPFS (immutable). To host it
yourself — so you can delete it later (GDPR / data-protection) — prepare a
draft: the platform builds and signs the exact VC without uploading it, you
host those bytes at your own https:// URL, then anchor that URL on-chain.
# `pd`: a PassportData built as in "Item passports & clear-content hash" above.
draft = client.prepare_self_hosted_item(
application=app_id,
network="StarknetMainnet",
item_number="ADO-LEAD-001",
data=pd.model_dump(), # same content rules as create_passport_item
content_hash=pd.content_hash,
)
cert_url = "https://certs.example.com/ADO-LEAD-001.jsonld"
# 1. Host draft.certificate (the signed VC, a JSON-LD dict) at cert_url using
# YOUR OWN infra (S3, a static server, …) — it must be reachable over https.
upload_to_your_host(cert_url, draft.certificate) # <- your code, not the SDK
# 2. Anchor that URL on-chain — reuses the exact content captured above:
passport = draft.create(certificate_uri=cert_url)
# ...or point an existing passport at the hosted credential:
# passport = draft.update(passport_id, certificate_uri=cert_url)
# Poll passport.certification_status as usual ("certified" once anchored).
The on-chain event anchors both the certificate_uri and the VC's signature
(proof.proofValue), so verify-dpp pins the served credential to its on-chain
record — a swapped or tampered document fails verification. A SelfHostedDraft
is single-use and fails closed if the prepared credential is unsigned.
certificate_uri / certificate_signature are also exposed directly on
create_passport_item() / update_passport_item() (and on Passport) for
callers signing/hosting through their own pipeline — but prepare_self_hosted_item
is the recommended path: it guarantees the hosted bytes and the on-chain content
hash match. See scripts/demo_self_hosted_cert.py for a runnable example.
Opt-in form upload
upload_opt_in_form() pins a consent-form image (PNG/JPEG/WebP) to IPFS and
returns an ipfs://<cid> URI. Add it under the reserved certificate.opt_in_form
path before computing content_hash, so the anchored hash covers the form
and verify-dpp can display it:
uri = client.upload_opt_in_form("opt-in.png") # or: content=<bytes>, media_type="image/jpeg"
pd = PassportData(name="ADO-LEAD-001", certificate={"opt_in_form": uri})
passport = client.create_passport_item(
application=app_id,
network="StarknetMainnet",
item_number="ADO-LEAD-001",
data=pd.model_dump(),
content_hash=pd.content_hash,
)
certificate.opt_in_form is auto-kept in the signed VC even under selective
certified_paths. See scripts/demo_adomos_lead.py for the full lead flow
(encryption + opt-in form + self-hosting).
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_item |
(*, application, network, item_number, model_number=None, product_name=None, data=None, certified_paths=None, content_hash=None, certificate_uri=None, certificate_signature=None) -> Passport |
Granularity is always item. content_hash (from PassportData.content_hash or compute_content_hash) is anchored on-chain in place of the server-computed hash. certificate_uri/certificate_signature self-host the credential (prefer prepare_self_hosted_item). No claim parameters — items created here are never minted. |
update_passport_item |
(passport_id, *, data=None, certified_paths=None, content_hash=None, certificate_uri=None, certificate_signature=None) -> Passport |
When changing data, resend the matching content_hash in the same call. |
prepare_self_hosted_item |
(*, application, network, item_number, model_number=None, product_name=None, data=None, certified_paths=None, content_hash=None) -> SelfHostedDraft |
Builds + signs the VC (via build-vc) without uploading; host draft.certificate, then draft.create/update(certificate_uri=...). |
upload_opt_in_form |
(file_path=None, *, content=None, media_type="image/png", filename=None) -> str |
Pins an opt-in form image to IPFS; returns ipfs://<cid>. Provide exactly one of file_path or content. |
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. |
SelfHostedDraft |
Returned by prepare_self_hosted_item. .certificate (signed VC to host), .create(*, certificate_uri), .update(passport_id, *, certificate_uri). Single-use; reuses the prepared content so the hosted VC matches the on-chain hash. |
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. |
compute_content_hash |
(data: Dict) -> str — felt252-masked SHA-256 of the JCS-canonicalized clear data; what contentHash must contain. |
CONTENT_HASH_PATTERN |
Compiled regex a valid content hash must match (^0x0[0-7][0-9a-f]{62}$). |
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 creates "item" only; 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. |
certificate_uri |
Optional[str] |
https URL of a self-hosted (deletable) signed VC, set instead of ipfs_cid when the issuer hosts the credential. |
certificate_signature |
Optional[str] |
The self-hosted VC's signature (proof.proofValue), anchored on-chain so verify-dpp pins the served credential. |
certification_status |
Optional[str] |
"pending" while the certify job is queued or retrying, "certified" once the on-chain transaction succeeds, "error" once all retries are exhausted. Read-only. |
certified_at |
Optional[datetime] |
UTC timestamp of the last successful certification, None until the cert job completes. Read-only. |
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
The client targets these DPP endpoints:
| Endpoint | Method |
|---|---|
/v1/dpp/passports |
GET, POST |
/v1/dpp/passports/:id |
GET, PATCH |
/v1/dpp/passports/build-vc |
POST (used by prepare_self_hosted_item) |
/v1/dpp/opt-in-forms |
POST (used by upload_opt_in_form) |
Migrating to 3.0
The model write methods are gone: since the backend only certifies free-form
data on item passports, the model write path no longer produced any
certification (no VC, no IPFS CID, no on-chain event). Reads are unchanged.
| 2.x | 3.0 |
|---|---|
create_passport_model(model_number=...) |
create_passport_item(item_number=...) |
update_passport_model(id, ...) |
update_passport_item(id, ...) |
product_id= (was a silent no-op) |
removed |
Everything else (reads, PassportData, encryption, filters, errors) is
unchanged. While migrating, consider anchoring the clear-content hash — see
"Item passports & clear-content hash" above.
Migrating to 2.0
Historical. The
create_passport_model/update_passport_modelmethods referenced below were the 2.x API and are removed in 3.0. Coming from 1.x or earlier, apply these steps then "Migrating to 3.0" above (create_passport_model→create_passport_item).
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:
- 3.0.0 — BREAKING: model write methods removed, writes are item-granularity (
create_passport_item/update_passport_item). Added clear-content hash anchoring (content_hash,compute_content_hash,PassportData.content_hash/.clear_data). See "Migrating to 3.0" above. - 2.1.0 —
Passport.certification_status/certified_atadded; recommended polling pattern switched tocertification_status == "certified". - 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-3.0.0.tar.gz.
File metadata
- Download URL: keyban_api_client-3.0.0.tar.gz
- Upload date:
- Size: 32.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
511fbb45354ceca696817f6f0ae55384b8573314998fe6db51d01601e1ecf382
|
|
| MD5 |
d1b2d6a114c8970598d22f1b09a0affe
|
|
| BLAKE2b-256 |
c70aac30bc80e8992f56edacf4fee853c2894edb463e393eed66e005c1067492
|
File details
Details for the file keyban_api_client-3.0.0-py3-none-any.whl.
File metadata
- Download URL: keyban_api_client-3.0.0-py3-none-any.whl
- Upload date:
- Size: 23.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b4162886c7c24ad66fcbf679b9cac01a42e17b0bf7d35ce7b1f2b037f76f04f9
|
|
| MD5 |
576120f5b3cf57009eb392d3464cf42b
|
|
| BLAKE2b-256 |
19264c27fb164f6ee142812c6d23024ef70780881c1cf0f51db1842e329741eb
|