Skip to content

DID-7740: [SDK-PY] Official Python server SDK: complete API surface, webhooks, Authentication, Transactions, generated types and tests - #1

Merged
arosasg merged 3 commits into
mainfrom
botinc/run-a79f155d
Oct 22, 2026
Merged

arosasg merged 3 commits into
mainfrom
botinc/run-a79f155d

Conversation

@botinc-agent

@botinc-agent botinc-agent Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

The official Didit Python library (didit-sdk, import didit) for every Didit server API: sessions, standalone checks, KYB, users and businesses, workflows, webhooks, cases, Transactions, wallet screening, Travel Rule and account management. It has full types, safe webhook verification and an Authentication module, and it regenerates itself from openapi/ (DID-7740, parent DID-7736).

What is in it

  • Generated layer (src/didit/types, src/didit/resources, tests/generated), produced by codegen/ from openapi/ only:
    • 257 typed methods: 248 server operations of verification.json plus the 9 account.json operations, in 50 resources.
    • Didit (sync, httpx.Client) and AsyncDidit (httpx.AsyncClient) come from the same templates.
    • Pydantic v2 response models keep unknown fields; request params are TypedDicts with NOT_GIVEN.
    • Application-scoped paths are filled from client options; multipart uploads, binary responses and auto-pagination are covered.
    • CI regenerates and fails on any difference, and an operation without SDK naming fails the generator with its name.
  • Core layer (DESIGN 6 to 8): client options with environment fallbacks, User-Agent and X-Didit-Client headers, the error hierarchy, the retry policy (POST and PATCH are never retried once they may have reached the server), timeouts, raw and streamed responses, origin pinning for pagination links, redaction and DIDIT_LOG=debug.
  • Webhooks (DESIGN 9): verify and verify_parsed, every signature header including the -Previous rotation variants, canonical JSON byte for byte, opt-in Simple signatures and typed events with UnknownEvent. All 25 backend vectors pass.
  • Authentication (didit-sdk[auth], DESIGN 10):
    • Discovery and JWKS cache; verify_access_token and verify_id_token (ES256 only).
    • PKCE, state and nonce helpers; code exchange and refresh (never retried).
    • Session get and revoke; step-up create and retrieve; publishable key parsing.
  • Spec sync (.github/workflows/spec-sync.yml, DESIGN 3.1): daily, opens chore(spec): sync API spec YYYY-MM-DD with an operation-level change summary.
  • Release plumbing (not run): release-please and release.yml with PyPI trusted publishing, triggered only by a published release.
  • Repository files: README with Language notes, examples/ (including FastAPI, Flask and Django webhook receivers), CONTRIBUTING, SECURITY, CODEOWNERS, issue and PR templates, Dependabot and a gitleaks CI job.

Review round 3: the requested change

Finding Change
The first spec sync after upstream adds x-didit-sdk (DID-7781) would go red: 12 tests in codegen/tests/test_codegen.py read names straight from openapi/overlay.json, which that sync empties The tests now read an operation's naming where it lives: its own x-didit-sdk first, then the overlay, as _count_operations already did. Tests that change a name change it in that same place. The overlay-drop test sets up its own starting point (naming only in the overlay), so it holds before and after upstream carries the naming. Only this test file changed.

Evidence (local):

  • Simulated the DID-7781 sync in a scratch copy: every overlay entry written upstream as x-didit-sdk in both specs, then codegen/spec_sync.py --from-dir. Result: no breaking changes, "Dropped 266 overlay entries that upstream now carries", and generate.py --check reports the generated code unchanged.
  • In that copy, codegen/tests went from 12 failed and 27 passed to 39 passed; the full suite the sync job runs passes (744 passed).
  • In the repository as it is: codegen/tests 39 passed (also on Python 3.10 and 3.14); full suite 744 passed with 98.92 % core coverage; ruff, ruff format, mypy, pyright and generate.py --check clean.

Review round 2: every requested change

Blocking

Finding Change
Windows CI red: .webp uploaded as application/octet-stream _files.guess_content_type uses a fixed table for every type the API accepts (jpg, png, webp, tiff, heic, pdf, zip, csv, ndjson, ...), the same on every OS, and falls back to mimetypes for other types. A test simulates a host whose mimetypes knows nothing.
A body that stalls or drops mid-read escapes as a raw httpx error and is not retried Reading the body is now inside the same handling as sending. GET, HEAD, PUT and DELETE are retried; POST and PATCH raise APITimeoutError or APIConnectionError. Error bodies follow the same rules. Streamed responses (iter_bytes, read, parse, write_to_file) raise the SDK's errors too. Covered by tests (sync and async) and by the real-socket quickstart run below.
Spec sync drops a method renamed upstream silently openapi/deprecated-operations.json is now keyed by resource.method. A rename upstream keeps the old name as a @deprecated alias of the same endpoint until remove_in, the next minor version, and is listed under "Breaking changes"; its old response type stays importable. Removed operations work as before. A name that moves to another path is compared old against new, and a breaking difference is listed. A name that now belongs to a different operation (swapped or reused) is breaking. An operation that becomes device-only is removed and listed as breaking, because server SDKs never expose those. An operation that loses its naming makes the generator fail until it is named.
Webhook input could raise something other than WebhookVerificationError A huge integer timestamp, deep nesting (in verify and verify_parsed) and a str body with a lone surrogate all give invalid_payload. secret=None (an unset environment variable), a bytes secret or a non-str entry raise ConfigurationError with a precise message. A negative tolerance is checked before the body.
PR description missing the Acceptance items This description: quickstart against a mocked server, test summary, public API tree and the registry setup.

Should fix

Finding Change
Discovery could send tokens to http or another host token_endpoint must be on the issuer's origin, and token_endpoint and jwks_uri must use https (http only for a loopback test issuer). A discovery document without issuer is refused (OIDC Discovery 4.3).
JWKS refetch limit ignored failed fetches A failed fetch (an issuer error, a bad response or a failed discovery) counts toward the one-per-minute limit. While no fresh key set exists after such a failure, verification fails fast with APIConnectionError instead of fetching again while holding the lock. It never raises TokenVerificationError for this, so an outage does not look like a bad token. A cancelled call and a ConfigurationError do not count.
A missing required response field raised AttributeError on access Lenient models set it to None. It stays out of model_fields_set and to_dict(), and to_dict()/to_json() no longer emit pydantic serializer warnings for off-schema values.
Spec sync saw only top-level field names; fixed counts turned sync PRs red A recursive schema diff covers nested objects, arrays, oneOf/anyOf, types (integer to number widening is allowed on requests), enum values (breaking on requests, open on responses), nullability and required fields, with direction-aware rules. Removed component schemas are breaking. The reviewer's case (DocumentAIDetectedCode.type from string to integer) is now reported as breaking on the three operations that return it. Test counts are derived from the JSON; none is hard-coded.
Pagination A repeated next link raises DiditError instead of looping. A relative next link resolves against the page URL, with the same origin check. A non-JSON or empty list response raises APIResponseValidationError.
Case-sensitive header merging duplicated headers Every layer (SDK defaults, default_headers, credentials, per-request headers) replaces a header of any case. Content-Type is set only when the caller did not set it. None removes a header, including httpx's own User-Agent.
The daily sync could overwrite a fix pushed to chore/spec-sync A guard step leaves the branch alone while it carries a commit that did not come from the sync bot, with a warning; merging or closing that PR lets the next run sync.
Deprecations never expired tests/test_contract.py fails once the package version reaches an entry's remove_in, which forces the deletion in that release PR (documented in CONTRIBUTING).
Em dashes in generated docstrings The generator writes upstream descriptions without them (763 removed); no file has one.
CODEOWNERS used the organisation It now names @didit-protocol/backend. That team needs write access to this repository; see the setup below.

Found by a second review of this revision, also fixed

  • Credentials are blanked by the exception classes themselves, so every error is covered, including APIResponseValidationError and errors from streamed reads. The form body of a token request (code, PKCE verifier, refresh token) is never kept on a request object.
  • The spec-sync guard looks only at an open sync pull request, so a squash-merged or closed one with a leftover branch never blocks later syncs.
  • A step-up amount string must be a plain ASCII decimal, because it is sent as written. A non-iterable webhook secret is a ConfigurationError.

Nits: I fixed all of these.

  • Webhooks:
    • Header names given as bytes (raw ASGI pairs) are decoded.
    • A float timestamp is missing_timestamp, and X-Timestamp must be plain ASCII digits.
    • The unreachable event fallback is removed.
  • Authentication:
    • An integer exp, nbf or iat too large for a float is malformed.
    • AsyncAuthentication keeps one lock per event loop.
    • OAuth errors read invalid_grant: <description>.
    • Step-up amount must be a finite decimal.
    • The example fails closed (required is False).
    • Errors blank the Authorization header on the request they carry.
    • The README says to deny-list on jti.
  • Core:
    • DIDIT_LOG documents and accepts debug only.
    • Response models can be passed back as request values.
    • NaN, Infinity and objects JSON cannot carry raise DiditError before sending.
    • Errors blank x-api-key on the request they carry.
  • Spec sync and release:
    • The sync refuses a download that is not an OpenAPI 3 document with paths, and writes nothing.
    • A dangling $ref does not crash the sync.
    • The generator's hint for a deprecated name that is live again says to delete that entry.
    • .release-please-manifest.json says 0.1.0, like the code.

Quickstart against a mocked server

examples/quickstart.py ran unchanged against a local stand-in for verification.didit.me on a real socket. The mock drops the connection partway through the first decision body, and the SDK retries the GET:

$ DIDIT_BASE_URL=http://127.0.0.1:18765 DIDIT_LOG=debug python examples/quickstart.py
[2026-10-17 18:02:50,137 didit DEBUG] POST /v3/session/ -> 201 in 3 ms (request id req_ec80bfff)
[2026-10-17 18:02:50,146 didit DEBUG] GET /v3/session/11111111-2222-3333-4444-555555555555/decision/ -> 200 in 1 ms (request id -)
[2026-10-17 18:02:50,146 didit DEBUG] GET /v3/session/11111111-2222-3333-4444-555555555555/decision/: RemoteProtocolError; retrying in 0.47 s (2 retries left)
[2026-10-17 18:02:50,622 didit DEBUG] GET /v3/session/11111111-2222-3333-4444-555555555555/decision/ -> 200 in 2 ms (request id req_a96b2321)
Send the user to: https://verify.didit.me/session/abc123def456
Status: Approved
Webhook verified: 11111111-2222-3333-4444-555555555555 Approved
exit 0

# Log of the local mock API (the first decision response drops the connection after 16 bytes of a 500-byte body):
mock-api: POST /v3/session/ key=present ua=didit-python/0.1.0 (CPython 3.12.15; Linux)
mock-api: GET /v3/session/11111111-2222-3333-4444-555555555555/decision/ key=present ua=didit-python/0.1.0 (CPython 3.12.15; Linux)
mock-api: GET /v3/session/11111111-2222-3333-4444-555555555555/decision/ key=present ua=didit-python/0.1.0 (CPython 3.12.15; Linux)

Tests and checks

CI for this revision runs once it is published; it changes only codegen/tests/test_codegen.py. CI on the previous head 4956888 (run 39056465333) is green: lint, both type checkers, the generated-code check, the build, the secret scan and the tests on Python 3.10 to 3.14 (Linux), 3.12 on macOS and 3.12 on Windows (744 passed; this job failed in the previous round). The live smoke job is skipped, as designed.

The same checks run locally:

Check Result
uv lock --check lock matches pyproject.toml
uv run ruff check . and uv run ruff format --check . clean (638 files)
uv run mypy (strict) no issues in 630 source files
uv run pyright 0 errors
uv run python codegen/generate.py --check up to date (567 generated files)
uv run pytest -m "not live" --cov --cov-fail-under=90 744 passed, core coverage 98.9 %
The same suite on Python 3.10 and 3.14 744 passed on each
uv build, install the wheel in a clean venv, import works without cryptography; didit/py.typed shipped
actionlint on every workflow no errors
gitleaks 8.30.1 with .gitleaks.toml (full history and working tree) no leaks

The 744 tests include:

  • 257 generated per-operation tests, each run on Didit and AsyncDidit;
  • the contract test (spec operations equal SDK methods, deprecations expire);
  • 104 webhook tests, including all 25 backend vectors and their byte-for-byte canonical_v2;
  • 197 Authentication tests;
  • the core, model, type-build and generator tests.
Command output tails
$ uv lock --check
Resolved 33 packages in 1ms
(exit 0)

$ uv run ruff check .
All checks passed!
(exit 0)

$ uv run ruff format --check .
638 files already formatted
(exit 0)

$ uv run mypy
Success: no issues found in 630 source files
(exit 0)

$ uv run pyright
0 errors, 0 warnings, 0 informations
(exit 0)

$ uv run python codegen/generate.py --check
Generated code is up to date (567 files).
(exit 0)

$ uv run pytest -m not live -q -p no:cacheprovider --cov --cov-report=term --cov-fail-under=90
----------------------------------------------------------
TOTAL                                   2309     25    99%
Required test coverage of 90% reached. Total coverage: 98.92%
744 passed, 4 deselected in 19.69s
(exit 0)

$ actionlint .github/workflows/*.yml   (1.7.7)
(exit 0)

$ uv run --python 3.10 pytest -m "not live" -q   (separate environment)
744 passed, 4 deselected in 12.49s
(exit 0)

$ uv run --python 3.14 pytest -m "not live" -q   (separate environment)
744 passed, 4 deselected in 10.11s
(exit 0)

$ uv build; install the wheel into a clean Python 3.12 venv; import didit, didit.authentication, didit.webhooks
Successfully built didit_sdk-0.1.0.tar.gz and didit_sdk-0.1.0-py3-none-any.whl
installed 0.1.0 cryptography installed: False (wheel contains didit/py.typed)
(exit 0)

$ gitleaks git . --config .gitleaks.toml --redact   (8.30.1, full history)
no leaks found
$ gitleaks dir . --config .gitleaks.toml --redact   (working tree with this revision)
no leaks found

Public API tree

All 257 API methods plus the webhook and Authentication entry points
# Public API tree of didit-sdk 0.1.0: client.<resource>.<method>(positional path ids, required keyword args=, ...) -> return type
# The same tree exists on AsyncDidit, where every method is awaitable (list methods: await or async for).
# 259 methods.

client.account.applications.create(org_id, ...) -> Application
client.account.applications.list(org_id) -> object
client.account.applications.retrieve(org_id, app_id) -> Application
client.account.applications.update(org_id, app_id, ...) -> Application
client.account.login(email=, password=) -> LoginResponse
client.account.organizations.list() -> list[Organization]
client.account.register(email=, password=) -> RegisterResponse
client.account.resend_otp(email=, password=) -> object
client.account.verify_email(email=, code=) -> VerifyEmailResponse
client.age_estimation.estimate(user_image=, ...) -> AgeEstimationEstimateResponse
client.aml.screen(full_name=, ...) -> AmlScreenResponse
client.aml_monitoring.retrieve_job(job_id) -> AmlMonitoringRetrieveJobResponse
client.billing.retrieve_balance() -> BillingRetrieveBalanceResponse
client.billing.top_up(amount_in_dollars=, ...) -> BillingTopUpResponse
client.biometric_templates.batch_delete(...) -> BiometricTemplatesBatchDeleteResponse
client.biometric_templates.count(...) -> BiometricTemplatesCountResponse
client.biometric_templates.delete(template_uuid) -> BiometricTemplatesDeleteResponse
client.biometric_templates.list(...) -> SyncPage[BiometricTemplatesListResponseResults]
client.biometric_templates.retrieve(template_uuid) -> BiometricTemplatesRetrieveResponse
client.business_sessions.list_networks(business_session_id) -> BusinessSessionsListNetworksResponse
client.businesses.batch_delete(...) -> BusinessesBatchDeleteResponse
client.businesses.create(...) -> BusinessDetailItem
client.businesses.list(...) -> SyncPage[BusinessListItem]
client.businesses.list_networks(vendor_business_uuid) -> BusinessesListNetworksResponse
client.businesses.retrieve(vendor_data) -> BusinessDetailItem
client.businesses.retrieve_ongoing_monitoring_job(job_id) -> BusinessesRetrieveOngoingMonitoringJobResponse
client.businesses.retrieve_session_ongoing_monitoring_job(job_id) -> BusinessesRetrieveSessionOngoingMonitoringJobResponse
client.businesses.set_aml_monitoring(is_enabled=, ...) -> BusinessesSetAmlMonitoringResponseVariant1 | BusinessesSetAmlMonitoringResponseVariant2 | BusinessesSetAmlMonitoringResponse202
client.businesses.set_ongoing_monitoring(is_enabled=, ...) -> BusinessesSetOngoingMonitoringResponseVariant1 | BusinessesSetOngoingMonitoringResponseVariant2 | BusinessesSetOngoingMonitoringResponse202
client.businesses.set_session_ongoing_monitoring(is_enabled=, ...) -> BusinessesSetSessionOngoingMonitoringResponseVariant1 | BusinessesSetSessionOngoingMonitoringResponseVariant2 | BusinessesSetSessionOngoingMonitoringResponse202
client.businesses.update(vendor_data, ...) -> BusinessDetailItem
client.businesses.update_status(vendor_data, status=) -> BusinessDetailItem
client.cases.apply_entity_action(case_uuid, action=, ...) -> CaseActionCaseEntityActionResult
client.cases.approve_resolution(case_uuid) -> CaseDetail
client.cases.assign(case_uuid, assignee_user_id=, ...) -> CaseDetail
client.cases.blueprints.create(name=, assignee_pool=, ...) -> CaseBlueprintDetail
client.cases.blueprints.delete(blueprint_uuid) -> None
client.cases.blueprints.discard_draft(blueprint_uuid, expected_version=, expected_revision=) -> None
client.cases.blueprints.install_presets(...) -> CasesBlueprintsInstallPresetsResponse
client.cases.blueprints.list(...) -> SyncPage[CaseBlueprintListItem]
client.cases.blueprints.list_history(blueprint_uuid, ...) -> SyncPage[CasePublicationPolicyRevision]
client.cases.blueprints.list_presets() -> CasesBlueprintsListPresetsResponse
client.cases.blueprints.publish(blueprint_uuid, expected_version=, expected_revision=) -> CasePublicationCaseBlueprint
client.cases.blueprints.retrieve(blueprint_uuid) -> CaseBlueprintDetail
client.cases.blueprints.retrieve_draft(blueprint_uuid) -> CasePublicationPolicyDraftResponse
client.cases.blueprints.save_draft(blueprint_uuid, expected_version=, expected_revision=, changes=) -> CasePublicationPolicyDraft
client.cases.blueprints.update(blueprint_uuid, ...) -> CaseBlueprintDetail
client.cases.bulk_assign(case_uuids=, assignee_user_id=, ...) -> CasesBulkAssignResponse
client.cases.bulk_update_status(case_uuids=, status=, ...) -> CasesBulkUpdateStatusResponse
client.cases.checklist.list(case_uuid) -> list[CaseChecklistItem]
client.cases.checklist.toggle_item(case_uuid, item_uuid, is_completed=) -> CaseChecklistItem
client.cases.create(title=, ...) -> CaseDetail
client.cases.delete(case_uuid) -> None
client.cases.escalate(case_uuid) -> CaseDetail
client.cases.find_duplicates(...) -> CaseDuplicateCaseDuplicatePage
client.cases.links.create(case_uuid, entity_type=, entity_uuid=) -> CaseLink
client.cases.links.delete(case_uuid, link_uuid) -> None
client.cases.links.list(case_uuid) -> list[CaseLink]
client.cases.list(...) -> SyncPage[CaseListItem]
client.cases.list_action_receipts(case_uuid, ...) -> CaseActionCaseActionReceiptPage
client.cases.list_aml_hits(case_uuid) -> list[dict[str, object]]
client.cases.list_events(case_uuid, ...) -> SyncPage[CaseEvent]
client.cases.list_reviewers() -> CasesListReviewersResponse
client.cases.notes.create(case_uuid, comment=, ...) -> CaseCollaborationCaseNote
client.cases.notes.create_attachment_upload(case_uuid, file=) -> CasesNotesCreateAttachmentUploadResponse
client.cases.notes.delete(case_uuid, note_uuid) -> None
client.cases.notes.list(case_uuid, ...) -> SyncPage[CaseCollaborationCaseNote]
client.cases.notes.retrieve_attachment_url(case_uuid, s3_key=) -> CasesNotesRetrieveAttachmentUrlResponse
client.cases.notes.update(case_uuid, note_uuid, ...) -> CaseCollaborationCaseNote
client.cases.print(case_uuid) -> CasesPrintResponse
client.cases.regulatory_reports.create(case_uuid, ...) -> RegulatoryReportDetail
client.cases.regulatory_reports.list(case_uuid, ...) -> SyncPage[RegulatoryReportDetail]
client.cases.reject_resolution(case_uuid, ...) -> CaseDetail
client.cases.reopen(case_uuid) -> CaseDetail
client.cases.request_info(case_uuid) -> CaseDetail
client.cases.requests.cancel(case_uuid, request_uuid) -> CustomerMessagingCaseRequest
client.cases.requests.create(case_uuid, contact_id=, purpose=, request_id=, expires_at=, preview_token=, ...) -> CustomerMessagingCaseRequest
client.cases.requests.list(case_uuid, ...) -> SyncPage[CustomerMessagingCaseRequest]
client.cases.requests.preview(case_uuid, contact_id=, purpose=, ...) -> CustomerMessagingCaseRequestPreviewResponse
client.cases.requests.retrieve_attachment(case_uuid, request_uuid) -> bytes
client.cases.requests.retrieve_options(case_uuid) -> CustomerMessagingCaseRequestOptions
client.cases.resolve(case_uuid, resolution=, ...) -> CaseDetail
client.cases.retrieve(case_uuid) -> CaseDetail
client.cases.retrieve_statistics(...) -> CasesRetrieveStatisticsResponse
client.cases.retry_action_receipt(case_uuid, receipt_uuid) -> CaseActionCaseActionReceipt
client.cases.routing_rules.create(name=, conditions=, blueprint=, ...) -> CaseRoutingRule
client.cases.routing_rules.discard_draft(record_uuid, expected_version=, expected_revision=) -> None
client.cases.routing_rules.list(...) -> SyncPage[CaseRoutingRule]
client.cases.routing_rules.list_history(record_uuid, ...) -> SyncPage[RoutingPublicationPolicyRevision]
client.cases.routing_rules.preview(source=, ...) -> CaseRoutingPreview
client.cases.routing_rules.publish(record_uuid, expected_version=, expected_revision=) -> RoutingPublicationCaseRoutingRule
client.cases.routing_rules.retrieve(record_uuid) -> CaseRoutingRule
client.cases.routing_rules.retrieve_draft(record_uuid) -> RoutingPublicationPolicyDraftResponse
client.cases.routing_rules.save_draft(record_uuid, expected_version=, expected_revision=, changes=) -> RoutingPublicationPolicyDraft
client.cases.routing_rules.update(record_uuid, ...) -> CaseRoutingRule
client.cases.teams.create(name=, members=, ...) -> CaseReviewTeam
client.cases.teams.list(...) -> SyncPage[CaseReviewTeam]
client.cases.teams.retrieve(record_uuid) -> CaseReviewTeam
client.cases.teams.update(record_uuid, ...) -> CaseReviewTeam
client.cases.transfer(case_uuid, target_blueprint=) -> CaseDetail
client.cases.update(case_uuid, ...) -> CaseDetail
client.customization.retrieve() -> BrandingCustomization
client.customization.update(...) -> BrandingCustomization
client.database_validation.validate(issuing_state=, ...) -> DatabaseValidationValidateResponse
client.document_ai.analyze(document=, fields=, ...) -> DocumentAiAnalyzeResponse
client.email.assess_risk(email=, ...) -> EmailRiskResponse
client.email.check_code(email=, code=, ...) -> EmailVerificationCheckResponse
client.email.send_code(email=, ...) -> EmailVerificationSendResponse
client.face_match.compare(user_image=, ref_image=, ...) -> FaceMatchCompareResponse
client.face_search.search(user_image=, ...) -> FaceSearchSearchResponse
client.fraud_checks.create(subject=, ...) -> FraudCheckResponse
client.fraud_checks.plan(subject=, ...) -> FraudCheckPlan
client.fraud_checks.retrieve(check_id) -> FraudCheckResponse
client.id_verification.verify(front_image=, ...) -> IdVerificationVerifyResponse
client.id_wallets.cancel_verification(request_id) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.list() -> DigitalIdWalletApiWalletCatalogResponse
client.id_wallets.poll_verification(request_id) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.retrieve_verification(request_id) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_audkenni(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_bankid_cz(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_bankid_no(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_bankid_se(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_buypass(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_connectid(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_diia(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_eparaksts_mobile(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_eudi(country=, return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_franceconnect(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_frejaid(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_ftn(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_govbr(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_govuk_wallet(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_idcard_ee(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_idin(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_itsme(country=, return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_mitid(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_mobile_id(country=, personal_code=, phone_number=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_mojeid(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_oneid(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_personalausweis(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_smart_id(country=, personal_code=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_uae_pass(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.id_wallets.start_vipps(return_url=, ...) -> DigitalIdWalletApiWalletVerificationResponse
client.ip.assess_risk(ip_address=, ...) -> IPRiskResponse
client.kyb.list_shareholders(kyb_response_id=, ...) -> KybListShareholdersResponse | KybListShareholdersResponse202
client.kyb.list_ubos(kyb_response_id=, ...) -> KybListUbosResponse | KybListUbosResponse202
client.kyb.search(country_code=, ...) -> KybSearchResponse
client.kyb.select(kyb_response_id=, ...) -> KybSelectResponse
client.networks.download_report(network_uuid) -> bytes
client.networks.list(...) -> SyncPage[NetworkListItem]
client.networks.list_members(network_uuid) -> NetworksListMembersResponse
client.networks.list_signals(network_uuid) -> NetworksListSignalsResponse
client.networks.notes.create(network_uuid, comment=, client_reference=) -> NetworkNote
client.networks.notes.delete(network_uuid, note_uuid) -> None
client.networks.notes.list(network_uuid, ...) -> SyncPage[NetworkNote]
client.networks.retrieve(network_uuid) -> NetworkDetail
client.networks.retrieve_graph(network_uuid, ...) -> NetworkGraph
client.networks.retrieve_map(network_uuid) -> NetworkMap
client.networks.retrieve_timeline(network_uuid) -> NetworksRetrieveTimelineResponse
client.passive_liveness.check(user_image=, ...) -> PassiveLivenessCheckResponse
client.phone.assess_risk(phone_number=, ...) -> PhoneRiskResponse
client.phone.check_code(phone_number=, code=, ...) -> PhoneVerificationCheckResponse
client.phone.send_code(phone_number=, ...) -> PhoneVerificationSendResponse
client.proof_of_address.verify(document=, ...) -> ProofOfAddressVerifyResponse
client.questionnaires.create(title=, languages=, form_elements=, ...) -> QuestionnaireDetail
client.questionnaires.delete(questionnaire_uuid) -> None
client.questionnaires.list(...) -> SyncPage[QuestionnaireListItem]
client.questionnaires.retrieve(questionnaire_uuid) -> QuestionnaireDetail
client.questionnaires.update(questionnaire_uuid, ...) -> QuestionnaireDetail
client.regulatory_reports.create(case=, template=, ...) -> RegulatoryReportDetail
client.regulatory_reports.delete(report_uuid) -> None
client.regulatory_reports.finalize(report_uuid) -> RegulatoryReportDetail
client.regulatory_reports.list(...) -> SyncPage[RegulatoryReportDetail]
client.regulatory_reports.retrieve(report_uuid) -> RegulatoryReportDetail
client.regulatory_reports.retrieve_download_url(report_uuid, ...) -> RegulatoryReportsRetrieveDownloadUrlResponse
client.regulatory_reports.update(report_uuid, ...) -> RegulatoryReportDetail
client.regulatory_reports.validate(report_uuid) -> RegulatoryReportsValidateResponse
client.report_templates.create(name=, jurisdiction=, report_type=, ...) -> ReportTemplateDetail
client.report_templates.delete(template_uuid) -> None
client.report_templates.list(...) -> SyncPage[ReportTemplateDetail]
client.report_templates.retrieve(template_uuid) -> ReportTemplateDetail
client.report_templates.update(template_uuid, ...) -> ReportTemplateDetail
client.sessions.batch_delete(...) -> SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202
client.sessions.bulk_update_aml_hit_status(session_id, hit_updates=, ...) -> object
client.sessions.create(workflow_id=, ...) -> SessionsCreateResponse
client.sessions.delete(session_id, ...) -> SessionsDeleteResponse
client.sessions.generate_pdf(session_id) -> bytes
client.sessions.import_shared(share_token=, trust_review=, workflow_id=, ...) -> object
client.sessions.imports.create(...) -> SessionImportJob
client.sessions.imports.list_errors(import_id, ...) -> SyncPage[SessionImportError]
client.sessions.imports.retrieve(import_id) -> SessionImportJob
client.sessions.imports.retrieve_template(...) -> bytes
client.sessions.list(...) -> SyncPage[SessionsListResponseResultsKycSessionRowSessionKindUser | SessionsListResponseResultsKybSessionRowSessionKindBusiness]
client.sessions.list_networks(session_id) -> SessionsListNetworksResponse
client.sessions.preview_resubmission(session_id, email_address=, ...) -> SessionsPreviewResubmissionResponse
client.sessions.retrieve_decision(session_id, ...) -> SessionsRetrieveDecisionResponse
client.sessions.retrieve_deletion_job(job_id) -> SessionsRetrieveDeletionJobResponse
client.sessions.reviews.create(session_id, ...) -> SessionsReviewsCreateResponse
client.sessions.reviews.list(session_id, ...) -> SyncPage[ReviewItem]
client.sessions.share(session_id, for_application_id=, ...) -> SessionsShareResponse
client.sessions.update_aml_hit_status(session_id, hit_id=, review_status=, ...) -> object
client.sessions.update_data(session_id, ...) -> SessionsUpdateDataResponse
client.sessions.update_feature_status(session_id, node_id, new_status=, ...) -> SessionsUpdateFeatureStatusResponse
client.sessions.update_kyb_company_data(session_id, company_uuid, ...) -> SessionsUpdateKybCompanyDataResponse
client.sessions.update_kyb_party(session_id, party_uuid, is_skipped=) -> SessionsUpdateKybPartyResponse
client.sessions.update_poa_data(session_id, ...) -> SessionsUpdatePoaDataResponse
client.sessions.update_status(session_id, new_status=, ...) -> SessionsUpdateStatusResponse
client.transactions.create(transaction_id=, transaction_category=, transaction_details=, subject=, ...) -> TransactionDetail
client.transactions.create_sdk_token(vendor_data=, ...) -> TransactionsCreateSdkTokenResponse
client.transactions.list(...) -> SyncPage[TransactionsListResponseResults]
client.transactions.list_networks(transaction_uuid) -> TransactionsListNetworksResponse
client.transactions.list_party_networks(transaction_uuid, transaction_party_uuid) -> TransactionsListPartyNetworksResponse
client.transactions.retrieve(transaction_id) -> TransactionDetail
client.transactions.rules.backtest(...) -> TransactionRuleBacktestResult
client.transactions.rules.create(title=, category=, mode=, ...) -> TransactionRuleDetail
client.transactions.rules.delete(rule_uuid) -> None
client.transactions.rules.install_library(...) -> TransactionRuleInstallResult
client.transactions.rules.list(...) -> SyncPage[TransactionRuleDetail]
client.transactions.rules.list_library(...) -> SyncPage[TransactionRuleLibraryEntry]
client.transactions.rules.retrieve(rule_uuid) -> TransactionRuleDetail
client.transactions.rules.uninstall_library(...) -> TransactionRuleInstallResult
client.transactions.rules.update(rule_uuid, ...) -> TransactionRuleDetail
client.transactions.travel_rule.confirm_ownership(transaction_id, confirmed=) -> TravelRuleTransferDetail
client.transactions.travel_rule.update_transfer(transaction_id, ...) -> TravelRuleTransferDetail
client.travel_rule.create_widget_session(wallet_address=, chain=, ...) -> TravelRuleWidgetSessionResponse
client.travel_rule.list_vasps(...) -> SyncPage[VaspDirectoryEntry]
client.travel_rule.register_inbound(chain=, txid=, wallet_address=, amount=, currency=, ...) -> TravelRuleInboundResponse
client.travel_rule.retrieve_settings() -> TravelRuleSettingsDetail
client.travel_rule.update_settings(...) -> TravelRuleSettingsDetail
client.travel_rule.wallet_addresses.create(address=, ...) -> WalletAddressEntry
client.travel_rule.wallet_addresses.delete(entry_uuid) -> None
client.travel_rule.wallet_addresses.list(...) -> SyncPage[WalletAddressEntry]
client.travel_rule.wallet_addresses.update(entry_uuid, ...) -> WalletAddressEntryUpdate
client.users.batch_delete(...) -> UsersBatchDeleteResponse
client.users.create(vendor_data=, ...) -> UserDetailItem
client.users.generate_pdf(vendor_data) -> bytes
client.users.list(...) -> SyncPage[UserListItem]
client.users.list_networks(vendor_user_uuid) -> UsersListNetworksResponse
client.users.retrieve(vendor_data) -> UserDetailItem
client.users.retrieve_ongoing_monitoring_job(job_id) -> UsersRetrieveOngoingMonitoringJobResponse
client.users.set_aml_monitoring(is_enabled=, ...) -> UsersSetAmlMonitoringResponseVariant1 | UsersSetAmlMonitoringResponseVariant2 | UsersSetAmlMonitoringResponse202
client.users.set_ongoing_monitoring(is_enabled=, ...) -> UsersSetOngoingMonitoringResponseVariant1 | UsersSetOngoingMonitoringResponseVariant2 | UsersSetOngoingMonitoringResponse202
client.users.update(vendor_data, ...) -> UserDetailItem
client.users.update_status(vendor_data, status=) -> UserDetailItem
client.users.upload_face(didit_internal_id, image=, ...) -> UsersUploadFaceResponse
client.wallet_screening.screen(wallet_address=, blockchain=, ...) -> WalletScreeningResult
client.wallet_screening.screen_pdf(provider=, screening_type=, risk_score=, severity=, status=, summary=, wallet_address=, blockchain=, source_of_funds=, destination_of_funds=, counterparty_connections=, sanctions_hit=, dominant_risk_category=, report_signature=, ...) -> bytes
client.webhooks.destinations.create(label=, url=, ...) -> WebhooksDestinationsCreateResponse
client.webhooks.destinations.delete(destination_uuid) -> None
client.webhooks.destinations.list() -> list[WebhooksDestinationsListResponseItem]
client.webhooks.destinations.retrieve(destination_uuid) -> WebhooksDestinationsRetrieveResponse
client.webhooks.destinations.update(destination_uuid, ...) -> WebhooksDestinationsUpdateResponse
client.webhooks.verify(raw_body, headers, secret, ...) -> WebhookEvent
client.webhooks.verify_parsed(payload, headers, secret, ...) -> WebhookEvent
client.workflows.create(features=, ...) -> dict[str, object]
client.workflows.delete(settings_uuid) -> None
client.workflows.list(...) -> SyncPage[WorkflowListItem]
client.workflows.retrieve(settings_uuid) -> object
client.workflows.update(settings_uuid, ...) -> dict[str, object]

# Webhooks (no API key needed): didit.webhooks.verify(raw_body, headers, secret, tolerance=300, allow_simple=False),
#   didit.webhooks.verify_parsed(payload, headers, secret, tolerance=300); also on client.webhooks.
# Authentication (didit-sdk[auth]): Authentication(issuer) / AsyncAuthentication(issuer), .from_publishable_key(pk),
#   verify_access_token, verify_id_token, create_pkce, create_state, create_nonce, exchange_code, refresh,
#   get_session, revoke_session, create_step_up, retrieve_step_up.

One-time setup before the first release (a person must do this)

  1. PyPI name: Didit must own the didit-sdk project. The transfer from Agustin Saiz is pending; the fallback name is didit-python (DESIGN 2.1).
  2. PyPI trusted publisher: owner didit-protocol, repository sdk-python, workflow release.yml, environment pypi.
  3. GitHub environment pypi: a required reviewer (Alex), limited to v* tags.
  4. Code owners: give the Backend team (@didit-protocol/backend) write access to this repository so that CODEOWNERS requests its review, or name the owning team there.
  5. Optional secrets: SPEC_SYNC_TOKEN and RELEASE_PLEASE_TOKEN.
    • Fine-grained, this repository only, with Contents and Pull requests write, plus Issues write for release-please.
    • They let the sync and release pull requests run CI.
  6. Optional live smoke tests: the DIDIT_SANDBOX_API_KEY secret and the DIDIT_SANDBOX_WORKFLOW_ID and DIDIT_SANDBOX_BASE_URL variables, for the manual job.
  7. Repository settings: allow GitHub Actions to create pull requests, turn on private vulnerability reporting, and protect main with the CI jobs.
  8. First release version: cut it with a Release-As: <version> footer (REL-PY uses 1.0.0). Release-please runs only when started by hand, and release.yml only on a published release, so merging publishes nothing.

Merge

Squash-merge this pull request with a conventional title, for example feat: official Python server SDK (DID-7740). Release-please reads only conventional commits.

Not verified here

  • No call reached the real Didit API or a real Authentication issuer, because the sandbox has no credentials. To run them: uv run pytest -m live with DIDIT_SANDBOX_API_KEY (and DIDIT_SANDBOX_WORKFLOW_ID).
  • Production next links are not checked. The SDK refuses any origin other than https://verification.didit.me; one live list call with more than one page settles it.

Parity: the core fixes in this revision touch behaviour that DESIGN.md describes: retries on body read errors, webhook input errors, pagination guards, Authentication discovery checks and the spec sync deprecations. They are listed in "Follow-ups for the supervisor" on DID-7740, so that the other six SDKs make the same changes.

Full diff: main...botinc/run-a79f155d

🤖 Generated with Claude Code

…webhooks, Authentication, Transactions, generated types and tests

Opened by a BotInc run. Delivery follows the workflow's review and merge policy.
…webhooks, Authentication, Transactions, generated types and tests

Opened by a BotInc run. Delivery follows the workflow's review and merge policy.
…webhooks, Authentication, Transactions, generated types and tests

Opened by a BotInc run. Delivery follows the workflow's review and merge policy.
@arosasg
arosasg merged commit 16a6b1c into main Oct 22, 2026
13 checks passed
@arosasg
arosasg deleted the botinc/run-a79f155d branch October 22, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant