Skip to main content

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: True with score: 0 is 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, adverseActionReasons and supportingFactors are empty by construction, and the one code present is informational. Never draw a Regulation B statement of specific reasons from it. dataState says 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 False so 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)

Source distribution for credda 0.1.0
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)

Table of built distributions (wheels) for credda 0.1.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page