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 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 data blob
  • Clear-content hash anchoring — anchor the hash of the PLAINTEXT data on-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 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_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:

  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
  4. Marks the passport as certification_status="certified" and stamps certified_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_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.

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/Infinity floats and integers beyond ±(2**53 − 1) raise ValueError (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 eq on application.id, status, product.idGranularity, product.modelNumber, product.batchNumber, product.itemNumber, mintedTo.id, and contains on product.name. Other field/operator combinations return HTTP 400. field uses the backend (camelCase) path — product.itemNumber, not item_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 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_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_model methods 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_modelcreate_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.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:

  • 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.0Passport.certification_status / certified_at added; recommended polling pattern switched to certification_status == "certified".
  • 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-3.0.0.tar.gz (32.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-3.0.0-py3-none-any.whl (23.1 kB view details)

Uploaded Python 3

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

Hashes for keyban_api_client-3.0.0.tar.gz
Algorithm Hash digest
SHA256 511fbb45354ceca696817f6f0ae55384b8573314998fe6db51d01601e1ecf382
MD5 d1b2d6a114c8970598d22f1b09a0affe
BLAKE2b-256 c70aac30bc80e8992f56edacf4fee853c2894edb463e393eed66e005c1067492

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for keyban_api_client-3.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b4162886c7c24ad66fcbf679b9cac01a42e17b0bf7d35ce7b1f2b037f76f04f9
MD5 576120f5b3cf57009eb392d3464cf42b
BLAKE2b-256 19264c27fb164f6ee142812c6d23024ef70780881c1cf0f51db1842e329741eb

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