credda — official Python SDK
Python client for the Credda Reliability Score API. Credda turns the activity that already happens on your platform into a portable, verifiable trust score your users own. You report outcome events; Credda scores them with a deterministic, bias-free formula; your users (and anyone they share with) can verify the result offline.
This is a faithful port of the TypeScript SDK (@credda/js) — same endpoints, same
request/response shapes, snake_case method names.
The score is a pure function of the append-only event ledger. No human and no AI ever sets, nudges, or edits it.
Install
pip install credda
Requires Python 3.9+. The only dependency is
httpx: no compiled extensions, no wheels to
match to your platform, nothing to build.
Reporting a bug
The source for this SDK is not public yet, so there is no issue tracker to
point you at. Email martin@credda.io and say what you called, what came
back, and what you expected. Please do not send an API key or a share token in
that message; a requestId from the failing response is enough for us to find
it.
Quickstart
1. Get a platform API key
Ask your Credda contact to provision a platform and issue an API key
(crd_live_…). A platform has a trust tier that determines how much weight its
reported events carry. Keep the key server-side — it authenticates every write.
import os
from credda import CreddaClient
credda = CreddaClient(api_key=os.environ["CREDDA_API_KEY"])
2. Report your users' activity
Every meaningful outcome on your platform is an event: a completed transaction, a fulfilled order, a delivered project, a resolved dispute. Report them as they happen.
Verification matters. Set isVerified: True when your platform attests the outcome
actually happened on your system — that is what builds trust. An unverifiable
self-claim earns none (a user with only unverified activity stays "Provisional").
credda.report_event(
{
"userId": "your-stable-user-id", # your id for the user; Credda maps it internally
"eventType": "CONTRACT_FULFILLED",
"isVerified": True, # your platform attests this happened
"completedAt": "2026-07-20T12:00:00.000Z",
"transactionValue": 4200, # optional — higher-value outcomes weigh more
"metadata": {"orderId": "98432"}, # optional — free-form, echoed back
},
idempotency_key="order-98432-completed", # retry-safe, exactly-once ingestion
)
Backfilling or streaming? Send up to 100 events in one call. It is partial
success — one bad or duplicate item never fails the rest — and each item takes its own
idempotencyKey:
result = credda.report_events([
{"userId": "u_1", "eventType": "CONTRACT_FULFILLED", "isVerified": True, "idempotencyKey": "order-1"},
{"userId": "u_2", "eventType": "TRANSACTION_COMPLETED", "isVerified": True, "idempotencyKey": "order-2"},
])
print(result["created"], result["duplicate"], result["failed"])
3. Read scores
score = credda.get_score("u_1")
print(score["finalScore"], score["scoreBand"])
# Up to 100 users in one call; unknown ids come back as {"userId": ..., "error": "not_found"}
batch = credda.get_scores(["u_1", "u_2", "u_3"])
scoreBand is a plain-language label (Distinguished to Provisional to At Risk), never a
percentile or ranking.
More views on the same deterministic score:
credda.get_score_explain("u_1") # plain-language factor breakdown
credda.get_score_components("u_1") # six independently 0-100-scored components
credda.get_score_delta("u_1") # why the score last moved (top driver)
credda.get_platforms("u_1") # platforms contributing to the score
credda.get_risk("u_1") # advisory anti-gaming signals (never affects the score)
credda.get_usage(days=30) # your own API consumption vs. tier quota
⚠️ Unmeasured is not zero
The single most important rule when reading anything score-shaped. Credda
distinguishes "we measured it and it was bad" from "there was nothing to
measure", and the API says which on the wire: an unmeasured rate is null
(Python None), never 0. A record with no history has not completed 0% of its
commitments; it has not been measured, and rendering 0 turns an absence into
a failing record.
Every affected value travels with a discriminator. Branch on it:
comps = credda.get_score_components("u_1")
# The whole-payload state first: is there anything to measure at all?
ds = comps.get("dataSufficiency")
if ds and ds["insufficientData"]:
print(ds["note"]) # safe to show verbatim
for c in comps["components"]:
if not c["available"]:
print(f"{c['label']:<16} not measured") # NEVER "0"
continue
print(f"{c['label']:<16} {c['score']:.0f}")
Reading score without the branch gives you the two classic failures:
TypeError: '<' not supported between instances of 'NoneType' and 'int', or the
string "None" interpolated into a message, or, worst of all, an or 0 that
silently renders a zero.
The discriminator for each surface:
| Read | Branch on | Values it guards |
|---|---|---|
get_score_components |
component["available"], dataSufficiency["insufficientData"] |
score |
get_score_explain |
factor["available"], dataSufficiency["insufficientData"] |
value, contribution |
get_score_explain()["reasonCodes"] |
insufficientData, dataState |
adverseActionReasons, supportingFactors, finalScore |
get_trust_summary |
evidence["insufficientData"] |
completionRate, onTimeRate |
get_reliability_report |
metrics["insufficientData"], metrics["dataState"] |
completionRate, onTimeRate, consistency, disputeRate |
project_score / analyze_document |
timeliness["projectionIsUpperBound"] |
projected (it is a best case, not a point estimate) |
Three things follow, each with a test in tests/test_unmeasured_is_not_zero.py:
- A measured zero is still real and must still be shown.
available: Truewithscore: 0is a record that genuinely completed nothing. Hiding real bad news is a worse failure than the substitution this rule exists to prevent. - An absent measurement is never an adverse reason. When
reasonCodes["insufficientData"]is true,adverseActionReasonsandsupportingFactorsare empty by construction, and the one code present isinformational. Never draw a Regulation B statement of specific reasons from it.dataStatesays whether the record has no outcomes at all (no_recorded_outcomes) or outcomes whose score has not been computed yet (score_not_yet_computed). - An older API is safe. These fields are additive; against a server that
predates them the key is simply absent. Use
c.get("available") is not Falseso an absent flag reads as unknown, not as a positive "unavailable".
The annotations for score, value, completionRate and friends stay
non-Optional on purpose: widening them is a breaking change for type-checked
consumers of the published package and is held for a version decision. The
discriminators are what make correct code possible today, and the docstrings on
each TypedDict in credda.types say exactly which field guards which value.
4. Paginate history and the timeline
Both accept limit + cursor and return nextCursor (None once exhausted):
page = credda.get_score_history("u_1", limit=50)
while True:
for snapshot in page["data"]:
...
if not page.get("nextCursor"):
break
page = credda.get_score_history("u_1", limit=50, cursor=page["nextCursor"])
# …or let the SDK follow the cursor for you:
for item in credda.iter_timeline("u_1", limit=50):
...
5. Project a score (what-if)
Read-only and forward-looking — it never writes a snapshot or touches the ledger:
projection = credda.project_score("u_1", [
{"eventType": "CONTRACT_FULFILLED", "isVerified": True, "stakeLevel": "HIGH"},
])
print(projection["current"]["finalScore"], "→", projection["projected"]["finalScore"])
6. Get notified when a score changes (webhooks)
Register an HTTPS endpoint to receive HMAC-signed score.updated /
score.band_changed / dispute.resolved events instead of polling. The signing secret
is returned once — store it.
created = credda.create_webhook({
"url": "https://your-app.example/credda/webhook",
"events": ["score.updated", "score.band_changed"],
"description": "prod",
})
secret = created["secret"] # whsec_… — shown ONCE
credda.list_webhooks()
credda.test_webhook(created["webhook"]["id"]) # synthetic signed delivery
credda.update_webhook(created["webhook"]["id"], {"isActive": False})
credda.get_webhook_deliveries(created["webhook"]["id"], limit=20)
credda.delete_webhook(created["webhook"]["id"])
Verifying deliveries
Credda signs each delivery with X-Credda-Signature: sha256=<hex> — an HMAC-SHA256 over
f"{X-Credda-Timestamp}.{raw_body}". Verify on the raw body, before parsing JSON, or
the bytes won't match.
from credda import construct_webhook_event, WebhookVerificationError
# Flask example
@app.post("/credda/webhook")
def credda_webhook():
try:
event = construct_webhook_event(
request.get_data(as_text=True), # RAW body
request.headers.get("X-Credda-Signature"),
WEBHOOK_SECRET,
request.headers.get("X-Credda-Timestamp"),
)
except WebhookVerificationError:
return "", 400
if event["type"] == "dispute.resolved":
... # event["data"]["disputeId"]
else:
... # event["data"]["score"], event["data"]["band"]
return "", 204
Prefer branching over exceptions? verify_webhook_signature(...) returns a result
object instead:
from credda import verify_webhook_signature
result = verify_webhook_signature(raw_body, signature_header, secret, timestamp_header)
if not result.valid:
log.warning("rejected webhook: %s", result.reason)
Deliveries older (or newer) than tolerance_seconds (default 300) are rejected as
possible replays; pass tolerance_seconds=0 to disable that check. You can also pass the
whole header mapping instead of the two values: verify_webhook_signature(body, None, secret, headers=request.headers).
7. Let users verify and carry their trust
Mint a share token for a user to get an embeddable trust badge and a public verify link. The public reads need no API key — safe from a browser:
minted = credda.mint_share_token("u_1")
print(minted["verifyUrl"], minted["embedSnippet"])
public = CreddaClient() # no key
trust = public.resolve_token(minted["token"]) # PII-free public payload
bundle = public.get_trust_export(minted["token"]) # portable, self-verifying export
credda.revoke_share_token("u_1") # invalidates every embed using it
Issuer discovery documents are public too:
public.get_did_document() # /.well-known/did.json
public.get_trust_registry() # /.well-known/credda-trust-registry.json
8. Resolve a dispute
credda.resolve_dispute("dsp_1", "FOR_USER") # or "AGAINST_USER"
9. Earn isVerified with a counterparty confirmation
report_event lets you assert isVerified. A confirmation request is the strong
form: you propose the outcome, hand the one-time token to the named counterparty over
your own channel, and the event is written — verified — only when that distinct party
confirms. Creating the request writes no event and touches no score.
created = credda.create_confirmation_request({
"userId": "user_123",
"eventType": "CONTRACT_FULFILLED",
"counterpartyRef": "client@example.com",
"counterpartyName": "Acme Studio",
"description": "Landing page rebuild, delivered 2026-07-20",
"expiresInDays": 14,
}, idempotency_key="job-9182:confirm")
# Shown ONCE — deliver it yourself; only the counterparty should hold it.
token = created["confirmationToken"]
send_your_own_email(created["confirmation"]["counterpartyRef"], created["previewUrl"])
# The counterparty side (no API key — the token is the capability):
credda.preview_confirmation(created["confirmation"]["id"], token)
result = credda.respond_to_confirmation(created["confirmation"]["id"], token, "confirm")
result["eventId"] # the verified event that was written
10. Watch a line with a threshold policy
A policy is declarative "notify me when a subject crosses THIS line" config that
delivers a policy.threshold_crossed webhook. It never reads into, blocks, or changes
a score.
credda.create_policy({
"name": "flag drops below 40",
"appliesToAll": True,
"metric": "score",
"direction": "down",
"threshold": 40,
})
11. Benchmark a subject against its cohort
credda.get_benchmarks() # public: dimensions + the k-anonymity floor
me = credda.get_user_benchmark("user_123", dimension="tenureBand")
if me["available"]:
print(me["percentile"], me["comparison"])
else:
print("suppressed:", me["reason"]) # insufficient_data | no_score
A cohort below the k-anonymity floor comes back available: False with no numbers.
A percentile is a distribution fact, never a verdict.
12. Query your book of subjects
page = credda.list_users(
score_min=40, band="Established", subject_type="PERSON",
has_verified_events=True, sort="score", order="desc", limit=100,
)
for subject in page["data"]:
print(subject["externalId"], subject["finalScore"], subject["verifiedEventCount"])
The filter surface is a closed, validated set — there is deliberately no free-form
query DSL, because the closed set is what lets the server guarantee tenant scoping
for every combination of filters. CSV export (?format=csv) is a raw-HTTP use case.
A subject whose score has not been computed yet reports finalScore: None and
scoreBand: None — never a placeholder number. Pass has_score=False to list
exactly those, or has_score=True to exclude them.
To size a segment before pulling it, get_book_summary takes the same filters and
answers with counts instead of rows:
summary = credda.get_book_summary(subject_type="ORGANIZATION", has_verified_events=True)
print(summary["matched"], summary["central"]["median"]) # median is None if nothing is scored
Errors
Any non-2xx response raises CreddaError, carrying the HTTP status, the API's own
error message, and the request path:
from credda import CreddaError
try:
credda.get_score("unknown-user")
except CreddaError as err:
print(err.status, err.message, err.path)
API surface
| Area | Methods |
|---|---|
| Public (no key) | resolve_token, get_trust_export, get_delivery_receipts, get_public_professional_record, get_public_reliability_report, get_public_career_export, get_did_document, get_trust_registry, get_credential_issuer_metadata |
| Public catalogs | get_plans, get_webhook_events, get_error_catalog, get_enums, get_reason_codes, get_outcome_templates, get_changelog, get_benchmarks, get_open_badge_achievements, get_open_badge_achievement |
| Platform read | get_score, get_scores, list_users, get_book_summary, get_score_explain, get_score_delta, get_score_components, get_score_history, get_timeline, get_platforms, get_risk, get_usage, get_usage_meters, get_quota, get_activity, export_events, get_trust_summary, get_dispatch_reliability |
| Platform analytics | get_event_analytics, get_score_analytics |
| Benchmarks | get_benchmark_distribution, get_user_benchmark |
| Verified Earnings | get_earnings, get_earnings_summary, mint_earnings_credential |
| Verified Profile | get_verified_profile, record_qualification |
| Professional Record | get_professional_record, mint_professional_record_credential |
| Reliability report | get_reliability_report, get_public_reliability_report |
| Career export | get_career_export, get_public_career_export |
| Projection | project_score |
| Writes | report_event, report_events, mint_share_token, revoke_share_token, resolve_dispute, create_credential_offer |
| Agents | register_agent, get_agent |
| Confirmation requests | create_confirmation_request, create_confirmation_batch, list_confirmations, get_confirmation, cancel_confirmation, preview_confirmation, respond_to_confirmation |
| Activation campaigns | create_activation_campaign, get_activation_campaign |
| Reference requests | create_reference_request, list_references, get_reference, cancel_reference, preview_reference, respond_to_reference |
| Threshold policies | create_policy, list_policies, get_policy, update_policy, delete_policy |
| Score monitors | create_monitor, list_monitors, get_monitor, update_monitor, delete_monitor |
| Bulk screenings | create_screening, list_screenings, get_screening, get_screening_results |
| Ingest & imports | ingest, create_mapping, list_mappings, get_mapping, delete_mapping, create_import, list_imports, get_import, get_import_errors |
| Webhook management | create_webhook, list_webhooks, update_webhook, delete_webhook, test_webhook, get_webhook_deliveries, replay_webhook_delivery, get_recent_webhook_events |
| Webhook verification | verify_webhook_signature, construct_webhook_event |
| Convenience | iter_score_history, iter_timeline |
Every authenticated method takes an optional trailing api_key argument that overrides
the constructor's key for that call.
Deliberately not ported from @credda/js
Every network method in the TypeScript SDK exists here under the same name in
snake_case — that consistency is the point, and a parity test asserts each one's
method, path, query and body match what @credda/js sends.
The one deliberate omission is offline credential verification —
verifyTrustCredential, verifyVerifiableCredential, isCredentialRevoked,
verifyTrustExport — which the TypeScript SDK does with WebCrypto EdDSA. Porting it
would mean adding a cryptography dependency, and this package is httpx-only on
purpose: one dependency, no build step, no wheels to match to your platform. It is an
omission with a workaround, not a missing capability. Every method that returns a
credential (get_trust_export, mint_earnings_credential,
mint_professional_record_credential, …) hands back the signed VC-JWT verbatim, so
verify it with a JOSE library (e.g. pyjwt[crypto] or python-jose) against the JWKS
in get_did_document(), and check revocation against the statusList URL on the
payload. Webhook signature verification is included (verify_webhook_signature —
HMAC-SHA256 from hashlib/hmac, no dependency needed) and is wire-compatible with
the TypeScript and Go SDKs.
Notes on shapes
Responses are returned as plain dicts with the API's own camelCase keys — nothing is
renamed or dropped, so a newly added server-side field is always visible. TypedDict
definitions for the main shapes live in credda.types for editor/mypy support.
Two arguments differ from the wire name because they collide with Python keywords:
get_score_history(from_=...) maps to the from query parameter, and the score/delta
response's from key must be read as payload["from"].
Development
pip install -e ".[dev]"
python -m pytest
Tests use httpx.MockTransport — no network access, no API key needed.
Full API reference
GET https://api.credda.io/openapi.json · interactive docs at
https://api.credda.io/docs.
Metadata
Release files for credda 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| credda-0.1.0.tar.gz | 80.6 kB | Without provenance | Details for credda-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| credda-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for credda-0.1.0-py3-none-any.whl |
Total release size: 142.6 kB
Release files / credda-0.1.0.tar.gz
| Download URL | credda-0.1.0.tar.gz |
|---|---|
| Size | 80.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bbea91bfb114b7df82be03a732692276b8c4097fc2538354ba50ab3a7bb98da9
|
|
BLAKE2b-256 checksum How to use checksums |
2f929c394fcfd55095040e1d8a064866de325ee67d160d5f14a6c3132f671670
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Release files / credda-0.1.0-py3-none-any.whl
| Download URL | credda-0.1.0-py3-none-any.whl |
|---|---|
| Size | 62.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
64b3f3c5e8abc229813c5e5f094ea0091338da220659504ca732491a1114b3df
|
|
BLAKE2b-256 checksum How to use checksums |
5e3ee43df5cfddffc553c68b604c63f0ac7a780034aa989b14a12a20976b7017
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.13
|
Security for credda 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.