diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1c72e55 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# Code owners of the Didit Python SDK. +# The Backend team owns the server SDKs. GitHub requests its review only once the team +# has write access to this repository (Settings > Collaborators and teams). +* @didit-protocol/backend diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..bc17fa3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,67 @@ +name: Bug report +description: Something in the Didit Python SDK does not work as documented. +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + **Never paste API keys, webhook secrets, access or refresh tokens, or personal data** + (names, documents, images, addresses, phone numbers, emails) into this issue. + Replace them with placeholders such as `YOUR_API_KEY`. + + To report a security vulnerability, do not open an issue: see [SECURITY.md](https://github.com/didit-protocol/sdk-python/blob/main/SECURITY.md). + - type: input + id: sdk-version + attributes: + label: SDK version + description: "Output of `python -c \"import didit; print(didit.__version__)\"`." + placeholder: "0.1.0" + validations: + required: true + - type: input + id: python-version + attributes: + label: Python version + placeholder: "3.12.7" + validations: + required: true + - type: input + id: os + attributes: + label: Operating system + placeholder: "Ubuntu 24.04, macOS 15, Windows 11" + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Minimal reproduction + description: The smallest code that shows the problem, with secrets and personal data removed. + render: python + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behaviour + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behaviour + description: What happened instead, including the full traceback. + validations: + required: true + - type: input + id: request-id + attributes: + label: Request id + description: The `request_id` of the failing API call, if any (from `APIStatusError.request_id` or the response headers). + - type: checkboxes + id: no-secrets + attributes: + label: Confirmation + options: + - label: I removed every API key, secret, token and piece of personal data from this issue. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec64d57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Didit documentation + url: https://docs.didit.me + about: API reference, guides and quickstarts. + - name: Report a security vulnerability + url: https://github.com/didit-protocol/sdk-python/security/advisories/new + about: Report vulnerabilities privately, never in a public issue. See SECURITY.md. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..cd77985 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,34 @@ +name: Feature request +description: Suggest an improvement to the Didit Python SDK. +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + The SDK is generated from the public Didit API, so a missing API capability is a + request for the API itself; describe it here anyway and we will route it. + Do not paste API keys, secrets or personal data. + - type: textarea + id: problem + attributes: + label: Problem + description: What are you trying to do, and what makes it hard today? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: The API you would like, ideally as a short code example. + render: python + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: textarea + id: context + attributes: + label: Additional context + description: Links, related issues, or other Didit SDKs (Node, Go, Java, PHP, Ruby, .NET) where this matters too. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..838153b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +# Weekly dependency updates, one grouped pull request per ecosystem. +# Commit messages follow conventional commits: "ci: ..." for actions, +# "chore(deps): ..." and "chore(deps-dev): ..." for Python packages. +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: ["*"] + commit-message: + prefix: ci + + - package-ecosystem: uv + directory: / + schedule: + interval: weekly + groups: + python-dependencies: + patterns: ["*"] + commit-message: + prefix: chore + prefix-development: chore + include: scope diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..68fd37b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,31 @@ +## Summary + + + +## Type of change + + + +- [ ] `fix`: a bug fix (patch) +- [ ] `feat`: a new capability (minor) +- [ ] `feat!` or `BREAKING CHANGE:`: a breaking change to the public SDK surface (major; minor while 0.x) +- [ ] `docs`, `test`, `refactor`, `perf`, `build`, `ci` or `chore`: no release on its own + +## Tests run + + + +- [ ] `uv run ruff check .` and `uv run ruff format --check .` +- [ ] `uv run mypy` and `uv run pyright` +- [ ] `uv run python codegen/generate.py --check` +- [ ] `uv run pytest` +- [ ] `uv run pytest -m live` (sandbox, only if relevant) + +## Checklist + +- [ ] Generated code (`src/didit/types`, `src/didit/resources`, `tests/generated`) was regenerated with `uv run python codegen/generate.py`, not edited by hand. +- [ ] The upstream copies in `openapi/` (`verification.json`, `account.json`, `webhook-vectors.json`) were not edited by hand. +- [ ] Public API changes have tests and a README update. +- [ ] No secrets, API keys or real customer data (fixtures are synthetic). + +Parity: same change in the other six SDK repositories, or not applicable because ... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8269999 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,220 @@ +# Continuous integration for the Didit Python SDK (docs/DESIGN.md section 11). +# Every third-party action is pinned by full commit SHA; Dependabot keeps the pins current. +name: CI + +on: + push: + branches: [main] + pull_request: + # Manual runs also start the live smoke tests (job live-smoke) when the + # DIDIT_SANDBOX_API_KEY secret is set. + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + # Cancel superseded runs of a pull request; every push to main gets its own run. + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + FORCE_COLOR: "1" + # Use uv.lock exactly as committed. The lint job checks that it matches pyproject.toml. + UV_FROZEN: "1" + +defaults: + run: + shell: bash + +jobs: + lint: + name: Lint (ruff) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: Check that uv.lock matches pyproject.toml + run: env -u UV_FROZEN uv lock --check + - name: Install dependencies + run: uv sync --frozen + - name: ruff check + run: uv run ruff check --output-format=github . + - name: ruff format --check + run: uv run ruff format --check . + + typecheck: + name: Type check (mypy, pyright) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: Install dependencies + run: uv sync --frozen + - name: mypy --strict + run: uv run mypy + - name: pyright + run: uv run pyright + + codegen: + name: Generated code is up to date + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: Install dependencies + run: uv sync --frozen + - name: Regenerate and compare with the committed code + # Fails when src/didit/types, src/didit/resources or tests/generated differ from + # what codegen/ produces from openapi/. Fix: uv run python codegen/generate.py + run: uv run python codegen/generate.py --check + + test: + name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + include: + # Adds the coverage gate to the existing Linux 3.12 job. + - os: ubuntu-latest + python-version: "3.12" + coverage: true + - os: macos-latest + python-version: "3.12" + - os: windows-latest + python-version: "3.12" + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv and Python ${{ matrix.python-version }} + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: uv sync --frozen + - name: pytest + if: ${{ !matrix.coverage }} + run: uv run pytest -m "not live" + - name: pytest with the core coverage gate (90 %) + if: ${{ matrix.coverage }} + run: uv run pytest -m "not live" --cov --cov-report=term-missing --cov-fail-under=90 + + build: + name: Build and install the wheel + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: Build the sdist and the wheel + run: uv build + - name: Install the wheel into a fresh virtual environment + run: | + uv venv --python 3.12 "$RUNNER_TEMP/wheel-venv" + uv pip install --python "$RUNNER_TEMP/wheel-venv" dist/*.whl + - name: Import didit without the optional cryptography dependency + # Run outside the checkout so that the import resolves to the installed wheel. + working-directory: ${{ runner.temp }} + run: | + py="$RUNNER_TEMP/wheel-venv/bin/python" + if "$py" -c "import cryptography" 2>/dev/null; then + echo "::error::cryptography is installed without the auth extra; it must stay optional (didit-sdk[auth])." + exit 1 + fi + "$py" -c "import didit; print(didit.__version__)" + + secret-scan: + name: Secret scan (gitleaks, full history) + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + # The official gitleaks-action needs a paid licence for organisations, so the + # release binary is downloaded and verified against its published SHA-256. + GITLEAKS_VERSION: "8.30.1" + GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" + steps: + - name: Check out the repository with its full history + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Install gitleaks ${{ env.GITLEAKS_VERSION }} + run: | + archive="$RUNNER_TEMP/gitleaks.tar.gz" + curl -sSfL --retry 3 -o "$archive" \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + echo "${GITLEAKS_SHA256} ${archive}" | sha256sum --check --strict + tar -xzf "$archive" -C "$RUNNER_TEMP" gitleaks + "$RUNNER_TEMP/gitleaks" version + - name: Scan every commit + # `gitleaks git` is the current name of `gitleaks detect` (deprecated in 8.19). + run: '"$RUNNER_TEMP/gitleaks" git --config .gitleaks.toml --redact --verbose --no-banner .' + + live-smoke: + name: Live smoke tests (sandbox) + # Never part of default CI (docs/DESIGN.md section 11): manual runs only, and only + # when the DIDIT_SANDBOX_API_KEY repository secret is set. + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Check for the sandbox API key + id: key + env: + DIDIT_SANDBOX_API_KEY: ${{ secrets.DIDIT_SANDBOX_API_KEY }} + run: | + if [ -n "$DIDIT_SANDBOX_API_KEY" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "::notice::DIDIT_SANDBOX_API_KEY is not set; the live smoke tests are skipped." + fi + - name: Check out the repository + if: steps.key.outputs.present == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv + if: steps.key.outputs.present == 'true' + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: Install dependencies + if: steps.key.outputs.present == 'true' + run: uv sync --frozen + - name: pytest -m live + if: steps.key.outputs.present == 'true' + # The key reaches only this step, through the environment; it is never printed. + # Optional repository variables: DIDIT_SANDBOX_WORKFLOW_ID, DIDIT_SANDBOX_BASE_URL. + env: + DIDIT_SANDBOX_API_KEY: ${{ secrets.DIDIT_SANDBOX_API_KEY }} + DIDIT_SANDBOX_WORKFLOW_ID: ${{ vars.DIDIT_SANDBOX_WORKFLOW_ID }} + SANDBOX_BASE_URL: ${{ vars.DIDIT_SANDBOX_BASE_URL }} + run: | + if [ -n "$SANDBOX_BASE_URL" ]; then + export DIDIT_SANDBOX_BASE_URL="$SANDBOX_BASE_URL" + fi + uv run pytest -m live diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..1e6a527 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,42 @@ +# NOTHING MAY BE PUBLISHED before Alex's explicit approval and the 2026-11-01 Didit release +# (docs/DESIGN.md section 13). The PyPI environment `pypi` used by release.yml must require +# a reviewer. +# +# release-please opens the release pull request (version bump and the generated +# CHANGELOG.md) and, once that pull request is merged and this workflow runs again, creates +# the tag and the GitHub release, which starts release.yml. It runs only when started by +# hand, so nothing is cut by accident. +# +# Token: a release or pull request created with the default GITHUB_TOKEN does not trigger +# other workflows, so release.yml would not start and the release pull request would get +# no CI. Add a repository secret RELEASE_PLEASE_TOKEN (a fine-grained token with Contents, +# Issues and Pull requests read and write on this repository only) to fix that; without it +# the workflow falls back to GITHUB_TOKEN. +name: Release Please + +on: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: release-please + cancel-in-progress: false + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + issues: write + pull-requests: write + steps: + - name: Run release-please + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3e93b4a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,108 @@ +# NOTHING MAY BE PUBLISHED before Alex's explicit approval and the 2026-11-01 Didit release +# (docs/DESIGN.md section 13). The GitHub environment `pypi` must require a reviewer, so +# that every upload to PyPI waits for an explicit approval. +# +# Publishes didit-sdk to PyPI with trusted publishing (OIDC, no API token) when a GitHub +# release is published. The PyPI project must list this repository, the workflow +# release.yml and the environment pypi as a trusted publisher. +name: Release + +on: + release: + types: [published] + +permissions: + contents: read + +concurrency: + group: release-${{ github.event.release.tag_name }} + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + build: + name: Build the distribution + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out the release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + with: + # No dependency cache in the release build. + enable-cache: false + - name: Check that the release tag matches the package version + env: + RELEASE_TAG: ${{ github.event.release.tag_name }} + run: | + uv run --no-project python - <<'PY' + import os + import pathlib + import re + import sys + import tomllib + + tag = os.environ["RELEASE_TAG"] + text = pathlib.Path("src/didit/_version.py").read_text(encoding="utf-8") + match = re.search(r'^__version__ = "([^"]+)"', text, re.MULTILINE) + if match is None: + print("::error::Could not read __version__ from src/didit/_version.py") + sys.exit(1) + version = match.group(1) + pyproject = pathlib.Path("pyproject.toml").read_text(encoding="utf-8") + project_version = tomllib.loads(pyproject)["project"]["version"] + + problems = [] + if tag != f"v{version}": + problems.append( + f"Release tag {tag!r} does not match src/didit/_version.py " + f"({version}); expected 'v{version}'." + ) + if project_version != version: + problems.append( + f"pyproject.toml version {project_version} does not match " + f"src/didit/_version.py ({version})." + ) + for problem in problems: + print(f"::error::{problem}") + if problems: + sys.exit(1) + print(f"Release tag {tag} matches version {version}.") + PY + - name: Build the sdist and the wheel + run: uv build + - name: Upload the distribution + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: dist + path: dist/ + if-no-files-found: error + retention-days: 7 + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + timeout-minutes: 10 + environment: + # Must require a reviewer (Settings > Environments > pypi > Required reviewers). + name: pypi + url: https://pypi.org/project/didit-sdk/ + permissions: + id-token: write # trusted publishing (OIDC) + steps: + - name: Download the distribution + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: dist + path: dist/ + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 + with: + packages-dir: dist/ diff --git a/.github/workflows/spec-sync.yml b/.github/workflows/spec-sync.yml new file mode 100644 index 0000000..9f61429 --- /dev/null +++ b/.github/workflows/spec-sync.yml @@ -0,0 +1,152 @@ +# Spec sync (docs/DESIGN.md section 3.1). +# +# Downloads the upstream OpenAPI documents into openapi/, regenerates the SDK, runs the +# checks and opens (or updates) one pull request titled +# "chore(spec): sync API spec YYYY-MM-DD" on the branch chore/spec-sync. +# +# Token: pull requests opened with the default GITHUB_TOKEN do not trigger CI (GitHub does +# not start workflows from events caused by GITHUB_TOKEN). To get CI on the sync pull +# request, add a repository secret SPEC_SYNC_TOKEN (a fine-grained token with Contents and +# Pull requests read and write on this repository only). Without it the workflow falls +# back to GITHUB_TOKEN; then close and reopen the pull request, or push to its branch, to +# start CI. +# +# Hand-pushed fixes: create-pull-request rebuilds chore/spec-sync from the default branch +# on every run, which would drop a fix pushed to that branch by hand (usually an +# openapi/overlay.json entry for a draft sync). While the open spec sync pull request +# carries a commit that did not come from this workflow, the run leaves it alone; once +# that pull request is merged or closed, the next run syncs whatever changed since (a +# leftover branch is rebuilt then). +name: Spec sync + +on: + schedule: + - cron: "23 6 * * *" # daily, 06:23 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: spec-sync + cancel-in-progress: false + +defaults: + run: + shell: bash + +jobs: + sync: + name: Sync the API spec + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + env: + FORCE_COLOR: "1" + UV_FROZEN: "1" + steps: + - name: Prepare + id: prepare + # The summary lives outside the checkout so that it is never committed. + run: | + echo "SUMMARY=$RUNNER_TEMP/spec-sync-summary.md" >> "$GITHUB_ENV" + echo "today=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + - name: Check out the repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # create-pull-request configures its own credentials from its token input. + persist-credentials: false + - name: Set up uv + uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 + - name: Install dependencies + run: uv sync --frozen + - name: Leave hand-pushed fixes on the sync branch alone + id: guard + env: + GH_TOKEN: ${{ github.token }} + BASE: ${{ github.event.repository.default_branch }} + BOT_EMAIL: 41898282+github-actions[bot]@users.noreply.github.com + run: | + open="$(gh pr list --repo "$GITHUB_REPOSITORY" --head chore/spec-sync --base "$BASE" \ + --state open --json number --jq 'length')" + if [ "$open" = "0" ]; then + echo "skip=false" >> "$GITHUB_OUTPUT" # no open sync pull request + exit 0 + fi + authors="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${BASE}...chore/spec-sync" \ + --jq '.commits[].commit.author.email')" + if printf '%s\n' "$authors" | grep -v -x -F -e "$BOT_EMAIL" -e '' | grep -q .; then + echo "::warning::chore/spec-sync has commits that did not come from the spec sync (a fix pushed by hand). This run leaves the branch alone: merge or close the spec sync pull request, and the next run syncs whatever changed since." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + - name: Download the upstream OpenAPI documents + id: sync + if: steps.guard.outputs.skip != 'true' + # Writes changed=true|false to $GITHUB_OUTPUT and a markdown summary of the added, + # removed and changed operations to $SUMMARY. + run: uv run python codegen/spec_sync.py --summary "$SUMMARY" + - name: Regenerate the SDK and run the checks + id: checks + if: steps.sync.outputs.changed == 'true' + run: | + failed=false + { + echo + echo "## Checks" + echo + } >> "$SUMMARY" + run_check() { + name="$1" + shift + echo "::group::$name" + if "$@"; then + echo "- [x] $name" >> "$SUMMARY" + else + echo "- [ ] $name: **failed**, see the workflow run" >> "$SUMMARY" + echo "::error::$name failed" + failed=true + fi + echo "::endgroup::" + } + run_check "Regenerate (codegen/generate.py)" uv run python codegen/generate.py + run_check "ruff check" uv run ruff check . + run_check "ruff format --check" uv run ruff format --check . + run_check "mypy --strict" uv run mypy + run_check "pyright" uv run pyright + run_check "pytest" uv run pytest -m "not live" + { + echo + echo "Opened by [spec sync run ${GITHUB_RUN_ID}](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})." + echo "Generated code comes from codegen/ and openapi/; do not edit it by hand." + } >> "$SUMMARY" + echo "failed=$failed" >> "$GITHUB_OUTPUT" + - name: Open or update the spec sync pull request + if: steps.sync.outputs.changed == 'true' + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.SPEC_SYNC_TOKEN || secrets.GITHUB_TOKEN }} + branch: chore/spec-sync + delete-branch: true + title: "chore(spec): sync API spec ${{ steps.prepare.outputs.today }}" + commit-message: "chore(spec): sync API spec ${{ steps.prepare.outputs.today }}" + body-path: ${{ env.SUMMARY }} + # A sync that fails a check still opens the pull request, as a draft, so the fix + # (usually an openapi/overlay.json entry) can be pushed to the same branch. + draft: ${{ steps.checks.outputs.failed == 'true' }} + # Only the spec copies and the generated layer; nothing else is committed. + add-paths: | + openapi + src/didit/types + src/didit/resources + tests/generated + author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + committer: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> + - name: Fail the run when a check failed + if: steps.checks.outputs.failed == 'true' + run: | + echo "::error::The spec sync pull request was opened, but at least one check failed. See the Checks section of the pull request." + exit 1 diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..a210c06 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,20 @@ +# gitleaks configuration, used by the secret-scan job in .github/workflows/ci.yml. +# Keep the allowlists narrow: never allowlist a directory or a whole rule. +title = "didit-protocol/sdk-python" + +[extend] +useDefault = true + +[[allowlists]] +description = "Upstream copies, synced byte for byte by the spec sync workflow and never edited by hand" +paths = [ + '''^openapi/verification\.json$''', + '''^openapi/account\.json$''', + '''^openapi/webhook-vectors\.json$''', +] + +[[allowlists]] +description = "Synthetic webhook secrets used by the test suite" +regexes = [ + '''^whsec_test_[A-Za-z0-9_]+$''', +] diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} diff --git a/AGENTS.md b/AGENTS.md index 7af6fd3..a21ea35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,27 @@ Status: in development, not released. uv for environments and locking, hatchling for the build, pytest, mypy --strict, pyright, ruff. +## Commands + +| Task | Command | +|---|---| +| Install (dev environment, from `uv.lock`) | `uv sync` | +| Generate `src/didit/types`, `src/didit/resources`, `tests/generated` from `openapi/` | `uv run python codegen/generate.py` | +| Check the committed generated code is current (CI) | `uv run python codegen/generate.py --check` | +| Sync `openapi/` with docs.didit.me (spec-sync workflow) | `uv run python codegen/spec_sync.py --summary spec-sync-summary.md` | +| Test (unit, generated, contract, webhooks, Authentication, codegen) | `uv run pytest` | +| Test with the core coverage gate | `uv run pytest --cov --cov-report=term-missing --cov-fail-under=90` | +| Live sandbox smoke tests (only with `DIDIT_SANDBOX_API_KEY`) | `uv run pytest -m live` | +| Lint and format | `uv run ruff check .` and `uv run ruff format .` | +| Type-check | `uv run mypy` and `uv run pyright` | +| Build the sdist and wheel | `uv build` | + +## Layout + +- Generated (never edit by hand): `src/didit/types/`, `src/didit/resources/`, `tests/generated/`. The generator is `codegen/didit_codegen/`; its tests are in `codegen/tests/`. +- Core (hand-written): everything else in `src/didit/` (`_base_client.py` transport and retries, `_client.py` options, `_exceptions.py`, `_pagination.py`, `_files.py`, `_response.py`, `_models.py`), `src/didit/webhooks/` and `src/didit/authentication/`. +- `openapi/deprecated-operations.json` (written by `codegen/spec_sync.py`) keeps methods whose operation was removed or renamed upstream as deprecated methods until `remove_in` (the next minor version); `tests/test_contract.py` fails once that version is reached, and the entry is then deleted. + ## Rules - Generated code is never edited by hand. Change the generator in `codegen/` or `openapi/overlay.json`, then regenerate; CI fails when the committed generated code is stale. diff --git a/CLAUDE.md b/CLAUDE.md index 7af6fd3..a21ea35 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,6 +12,27 @@ Status: in development, not released. uv for environments and locking, hatchling for the build, pytest, mypy --strict, pyright, ruff. +## Commands + +| Task | Command | +|---|---| +| Install (dev environment, from `uv.lock`) | `uv sync` | +| Generate `src/didit/types`, `src/didit/resources`, `tests/generated` from `openapi/` | `uv run python codegen/generate.py` | +| Check the committed generated code is current (CI) | `uv run python codegen/generate.py --check` | +| Sync `openapi/` with docs.didit.me (spec-sync workflow) | `uv run python codegen/spec_sync.py --summary spec-sync-summary.md` | +| Test (unit, generated, contract, webhooks, Authentication, codegen) | `uv run pytest` | +| Test with the core coverage gate | `uv run pytest --cov --cov-report=term-missing --cov-fail-under=90` | +| Live sandbox smoke tests (only with `DIDIT_SANDBOX_API_KEY`) | `uv run pytest -m live` | +| Lint and format | `uv run ruff check .` and `uv run ruff format .` | +| Type-check | `uv run mypy` and `uv run pyright` | +| Build the sdist and wheel | `uv build` | + +## Layout + +- Generated (never edit by hand): `src/didit/types/`, `src/didit/resources/`, `tests/generated/`. The generator is `codegen/didit_codegen/`; its tests are in `codegen/tests/`. +- Core (hand-written): everything else in `src/didit/` (`_base_client.py` transport and retries, `_client.py` options, `_exceptions.py`, `_pagination.py`, `_files.py`, `_response.py`, `_models.py`), `src/didit/webhooks/` and `src/didit/authentication/`. +- `openapi/deprecated-operations.json` (written by `codegen/spec_sync.py`) keeps methods whose operation was removed or renamed upstream as deprecated methods until `remove_in` (the next minor version); `tests/test_contract.py` fails once that version is reached, and the entry is then deleted. + ## Rules - Generated code is never edited by hand. Change the generator in `codegen/` or `openapi/overlay.json`, then regenerate; CI fails when the committed generated code is stale. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3f0b492 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing to the Didit Python SDK + +Thank you for helping. Read [docs/DESIGN.md](docs/DESIGN.md) first: it is the shared contract of all seven Didit server SDKs, and when the code and the design disagree, the code is wrong. + +## Setup + +You need [uv](https://docs.astral.sh/uv/). It installs the Python version from `.python-version` and the locked dependencies from `uv.lock`. + +```sh +uv sync --frozen +``` + +Run every command with `uv run` so that it uses the project environment. + +## The two layers + +**Generated layer**: `src/didit/types`, `src/didit/resources` and `tests/generated`. +These files are produced by `codegen/` from the OpenAPI documents in `openapi/` and start with `Code generated by codegen from openapi/*.json. DO NOT EDIT.` +Never edit them by hand. To change them, change the generator in `codegen/` or `openapi/overlay.json`, then regenerate: + +```sh +uv run python codegen/generate.py +``` + +CI runs `uv run python codegen/generate.py --check`, which regenerates into a temporary directory and fails when the result differs from what is committed. A hand edit, or a generator or overlay change committed without regenerating, fails that check. + +**Core layer**: everything else under `src/didit` (client, transport, retries, errors, pagination, files, webhooks, Authentication). It is hand-written, small and fully tested. + +## The OpenAPI documents + +- `openapi/verification.json`, `openapi/account.json` and `openapi/webhook-vectors.json` are upstream copies. Never edit them by hand; only the spec sync workflow (`.github/workflows/spec-sync.yml`) updates them. +- `openapi/overlay.json` holds the SDK naming (`x-didit-sdk`, DESIGN section 4) for operations whose upstream definition does not carry it yet. Keys are `" "` for `verification.json` and `"account "` for `account.json`; each value has `resource` (dot path of snake_case names, one nested accessor per segment), `method`, `auth`, `server` and, for application-scoped paths, `scope`. When upstream carries `x-didit-sdk` for an operation, the upstream value wins and the overlay entry is deleted. +- A new server operation without SDK naming makes the generator fail with a message that names the operation. The fix is an overlay entry (or `x-didit-sdk` upstream). + +The spec sync runs daily. When upstream changed, it regenerates the code, runs the checks and opens a pull request titled `chore(spec): sync API spec YYYY-MM-DD`. Its body lists added, removed, renamed and changed operations and schemas; field-level differences (nested fields, types, enum values, nullability, required fields) that can break code written against the SDK are listed under "Breaking changes". A download that is not an OpenAPI 3 document with paths is refused and nothing is written. + +When a sync fails a check, the pull request opens as a draft; push the fix (usually an `openapi/overlay.json` entry) to its branch `chore/spec-sync`. While that branch carries a commit that is not from the sync, later syncs leave it alone, so the fix is never overwritten: merge or close the pull request and the next sync picks up anything that changed since. + +### Deprecations + +The sync never deletes a public method silently (DESIGN section 3.1). When an operation is removed upstream, or its `x-didit-sdk` naming changes (a rename), `codegen/spec_sync.py` records the old method in `openapi/deprecated-operations.json`, keyed by `resource.method`, with a dereferenced copy of the operation and `remove_in`, the next minor version. The generator keeps it as a method marked `@deprecated` (a renamed method's old name calls the same endpoint as the new one). An operation that moves to another path with the same naming is not deprecated; the summary compares the old and new operation. Two cases are breaking and are not kept: a name that now belongs to a different operation (swapped or reused), and an operation that becomes a device or end-user token operation (`server: false`), which server SDKs never expose. An operation that loses its naming makes the generator fail until `openapi/overlay.json` names it again. + +- When the release that reaches `remove_in` is prepared (the release-please pull request), `tests/test_contract.py` fails until the entry is deleted: delete it, run `uv run python codegen/generate.py` and push to the release pull request. +- If a deprecated name becomes live again (for example an overlay entry gives a new path the old name), the generator says so; delete the entry. + +## Checks + +CI runs all of these; run them before you open a pull request. + +| Check | Command | +|---|---| +| Lint | `uv run ruff check .` | +| Formatting | `uv run ruff format --check .` (fix with `uv run ruff format .`) | +| Types | `uv run mypy` and `uv run pyright` | +| Generated code | `uv run python codegen/generate.py --check` | +| Tests | `uv run pytest` | +| Core coverage (90 % or more) | `uv run pytest --cov --cov-report=term-missing --cov-fail-under=90` | +| Build | `uv build` | + +Unit tests never touch the network. Fixtures are synthetic: never commit API keys, secrets or real customer data. CI also scans the full git history with gitleaks (`.gitleaks.toml`). + +## Live smoke tests + +`tests/test_live.py` talks to a Didit sandbox application and runs only when `DIDIT_SANDBOX_API_KEY` is set: + +```sh +DIDIT_SANDBOX_API_KEY=... uv run pytest -m live +``` + +Optional: `DIDIT_SANDBOX_WORKFLOW_ID` (enables the session test) and `DIDIT_SANDBOX_BASE_URL`. +They never run in the default CI. A maintainer can start them from the CI workflow (**Run workflow**) when the repository secret is set. + +## Commits, versions and the changelog + +Pull request titles and commits follow [Conventional Commits](https://www.conventionalcommits.org/): `fix:` is a patch, `feat:` a minor version, and `feat!:` or a `BREAKING CHANGE:` footer a major version (a minor version while the SDK is 0.x). Pick the type that matches the semver impact of the change. + +Releases are cut by release-please from those commits. It updates the version and generates `CHANGELOG.md`; never edit the changelog by hand. Squash-merge pull requests so that their conventional title becomes the commit release-please reads. `.release-please-manifest.json` holds the version in the repository (0.1.0, unreleased). The first public release sets its version with a `Release-As: ` footer in a commit on `main`. + +Nothing is published without Alex's explicit approval, and nothing before the 2026-11-01 Didit release. Do not push release tags or publish packages. + +## Keep the SDKs in sync + +Didit has seven server SDKs: `sdk-python`, `sdk-node`, `sdk-go`, `sdk-java`, `sdk-php`, `sdk-ruby` and `sdk-dotnet`. + +- Any public API change in `service-didit-verification` or `service-didit-auth` must reach the OpenAPI documents in mintlify-docs, and from there every server SDK through the spec sync pull request. +- A change to hand-written core behaviour that DESIGN.md describes (client options, errors, retries, pagination, webhooks, Authentication) is made in all seven repositories, one pull request each, and DESIGN.md is updated in all seven in the same words. +- Feature parity: a capability shipped in one server SDK ships in the other six. + +Every pull request states its parity: the same change in the other six repositories, or why it does not apply. + +## Public API changes + +A change to the public SDK surface needs tests, a README update and a conventional commit type that matches its semver impact. + +## Security + +Report vulnerabilities privately, never in an issue. See [SECURITY.md](SECURITY.md). diff --git a/README.md b/README.md index 965f02d..69bc901 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,284 @@ # Didit Python SDK -The official [Didit](https://didit.me) server-side library for Python. -Identity verification (KYC), business verification (KYB), AML screening, biometrics, transaction monitoring, Travel Rule and customer authentication, from your backend. +The official Didit Python library: identity verification (KYC), business verification (KYB), AML screening, biometrics, transaction monitoring, Travel Rule and customer authentication, from your Python backend. -> In development. Not released yet. +> In development, not released yet. The package will be published on PyPI as `didit-sdk` with the first release. +- Every server-side endpoint of the Didit API, with typed requests and responses +- `Didit` (blocking) and `AsyncDidit` (asyncio) clients with the same methods +- Webhook verification with every Didit signature scheme, and typed events +- Authentication helpers: verify the access and ID tokens your users get from Didit Authentication +- Retries, timeouts, auto-pagination, file uploads and raw response access built in + +## Install + +```sh +pip install didit-sdk # Python 3.10 or newer +pip install "didit-sdk[auth]" # adds token verification for Didit Authentication +``` + +## Quickstart + +Create an API key in the Business Console (Developers, API keys) and a workflow, then: + +```python +from didit import Didit + +client = Didit() # reads DIDIT_API_KEY + +# 1. Create a verification session and send the user to its URL. +session = client.sessions.create(workflow_id="", vendor_data="user-123") +print(session.url) + +# 2. Read the decision once the user is done (or when the webhook arrives). +decision = client.sessions.retrieve_decision(session.session_id) +print(decision.status) +``` + +```python +# 3. Verify the webhook Didit sends to your backend (Flask shown; see examples/). +import didit + + +@app.post("/webhooks/didit") +def didit_webhook(): + try: + event = didit.webhooks.verify(request.get_data(), request.headers, WEBHOOK_SECRET) + except didit.WebhookVerificationError as err: + return err.reason, 400 + if isinstance(event, didit.webhooks.SessionStatusUpdatedEvent): + print(event.session_id, event.status) + return "", 204 +``` + +More in [`examples/`](examples/): standalone ID verification, AML screening, transactions, Travel Rule, Authentication tokens and webhook receivers for FastAPI, Flask and Django. + +## The API + +Methods follow the API's resources: `client..(...)`. Path ids are positional arguments; everything else is a keyword argument. + +```python +from pathlib import Path + +client.sessions.list(status="Approved", limit=50) +client.sessions.imports.create(file=open("users.csv", "rb"), import_type="user_verification") +client.id_verification.verify(front_image=Path("front.jpg"), back_image=Path("back.jpg")) +client.aml.screen(full_name="Maria Garcia", date_of_birth="1992-03-14") +client.users.retrieve("user-123") +client.workflows.list() +client.webhooks.destinations.list() +client.cases.list() # application scoped, see Configuration +client.account.login(email="you@example.com", password="...") +``` + +The resource tree covers sessions (with imports and reviews), the standalone APIs (`id_verification`, `face_match`, `face_search`, `passive_liveness`, `age_estimation`, `proof_of_address`, `document_ai`, `aml`, `database_validation`, `phone`, `email`, `ip`, `kyb`), `id_wallets`, `fraud_checks`, `users`, `businesses`, `transactions`, `wallet_screening`, `travel_rule`, `workflows`, `questionnaires`, `webhooks.destinations`, `billing`, `biometric_templates`, `customization`, `cases`, `regulatory_reports`, `report_templates`, `networks` and `account`. The API reference at [docs.didit.me](https://docs.didit.me) documents every endpoint; each SDK method's docstring names the endpoint it calls. + +Request and response types live in `didit.types`, named after the method (`SessionsCreateResponse`, `SessionsCreateParams`) or after the API schema (`TransactionDetail`). Responses are [pydantic](https://docs.pydantic.dev) models: + +```python +decision = client.sessions.retrieve_decision(session_id) +decision.status # typed attributes +decision.to_dict() # plain JSON data, with the API's field names +decision.model_extra # fields this SDK version does not know yet +"aml" in decision.model_fields_set # was the field present (even as null)? +``` + +## Transactions and Travel Rule + +```python +transaction = client.transactions.create( + transaction_id="your-txn-001", + transaction_category="finance", + transaction_details={ + "direction": "outbound", + "amount": 1500, + "currency": "EUR", + "action_type": "withdrawal", + }, + subject={"entity_type": "individual", "vendor_data": "user-12345", "full_name": "Maria Garcia"}, +) +client.transactions.rules.list() +client.transactions.travel_rule.confirm_ownership("your-txn-001", confirmed=True) +client.wallet_screening.screen(wallet_address="bc1q...", blockchain="BTC") +client.travel_rule.list_vasps(search="exchange") +client.travel_rule.wallet_addresses.create(address="bc1q...", chain="BTC") +``` + +Their webhooks arrive as `TransactionCreatedEvent`, `TransactionStatusUpdatedEvent` and `TravelRuleStatusUpdatedEvent`. + +## Configuration + +```python +client = Didit( + api_key="...", # default: DIDIT_API_KEY + organization_id="...", # default: DIDIT_ORGANIZATION_ID + application_id="...", # default: DIDIT_APPLICATION_ID + base_url="https://verification.didit.me", + timeout=60, # seconds, or an httpx.Timeout + max_retries=2, + default_headers={"X-My-Header": "..."}, + http_client=httpx.Client(proxy="http://proxy:8080"), # optional +) +client.with_options(timeout=5, max_retries=0).sessions.list() +``` + +- Methods under `/v3/organization/{organization_id}/application/{application_id}/` (cases, networks, regulatory reports, report templates and some user and business tools) take both ids from the client options. Without them they raise `ConfigurationError` before sending anything. +- The `account` methods talk to `https://apx.didit.me/auth/v2` and never send the API key. `client.account.login(...)` returns an `access_token`; pass it as `account_token` (`client.with_options(account_token=...)`) for the methods that need it. +- Every method also accepts `extra_headers`, `extra_query`, `extra_body` and `timeout` for one request. +- One client per process is the intended pattern: it keeps a connection pool and is safe to share between threads. `with Didit() as client:` closes the pool at the end. +- `DIDIT_LOG=debug` logs method, path, status, duration and request id of every request to stderr (never bodies or headers). The logger is called `didit`. + +## Errors + +```python +import didit + +try: + client.sessions.retrieve_decision("...") +except didit.NotFoundError: + ... +except didit.RateLimitError as err: + print(err.status, err.request_id, err.headers.get("retry-after")) +except didit.APIStatusError as err: # any other non-2xx + print(err.status, err.body) +except didit.APIConnectionError: # no response (APITimeoutError is a subclass) + ... +``` + +| Error | When | +|---|---| +| `DiditError` | Base of everything below | +| `APIConnectionError`, `APITimeoutError` | No response | +| `APIStatusError` | Any non-2xx, with `status`, `body`, `headers`, `request_id`, `response` | +| `BadRequestError` 400, `AuthenticationError` 401, `PermissionDeniedError` 403, `NotFoundError` 404, `ConflictError` 409, `UnprocessableEntityError` 422, `RateLimitError` 429, `InternalServerError` 5xx | Subclasses of `APIStatusError` | +| `WebhookVerificationError` | A webhook failed verification; see `reason` | +| `ConfigurationError` | Missing API key, account token, organization or application id, or webhook secret | +| `APIResponseValidationError` | A successful response was not the documented content (for example HTML instead of JSON) | + +A response that does not match the published schema never raises: the SDK returns it with the fields it could read, and unknown values stay available. A field the response left out reads `None` (and is absent from `model_fields_set` and `to_dict()`). A failure while the body is still arriving is an `APIConnectionError` (or `APITimeoutError`), retried like any other for the methods above. + +### Retries and timeouts + +Failed requests are retried up to `max_retries` times (default 2) with exponential backoff (from 0.5 s, capped at 8 s, with jitter), honouring `Retry-After`. `GET`, `HEAD`, `PUT` and `DELETE` are retried on connection errors, timeouts, 408, 429 and 5xx. `POST` and `PATCH` are retried only when the connection failed before the request was sent, or on 429, so a create is never sent twice. Where an endpoint accepts an idempotency key (`idempotency_key=`), use it. + +## Pagination + +List methods return a page with `count`, `next`, `previous` and `results`. Iterating it walks every item of every page: + +```python +for session in client.sessions.list(limit=100): + print(session.session_id) + +page = client.sessions.list(limit=100) +page.results, page.count, page.has_next_page() +next_page = page.get_next_page() + +async for session in async_client.sessions.list(limit=100): + ... +``` + +Next links are followed only on the API's own origin (a relative link resolves against the page that returned it); the API key is never sent anywhere else. A list that keeps returning a link it already returned raises `DiditError` instead of looping forever. + +## Files and binary responses + +Upload parameters accept a path (`pathlib.Path`), bytes, an open binary file, or a tuple `(filename, content)` or `(filename, content, content_type)`. Endpoints that return files (session PDFs, network reports, wallet screening PDFs) return `bytes`; stream large ones: + +```python +pdf = client.sessions.generate_pdf(session_id) + +with client.sessions.with_streaming_response.generate_pdf(session_id) as response: + response.write_to_file("session.pdf") +``` + +## Raw responses + +```python +response = client.sessions.with_raw_response.retrieve_decision(session_id) +response.status_code, response.headers, response.request_id +decision = response.parse() +``` + +## Async + +```python +import asyncio +from didit import AsyncDidit + + +async def main() -> None: + async with AsyncDidit() as client: + session = await client.sessions.create(workflow_id="...") + async for row in client.sessions.list(): + ... + + +asyncio.run(main()) ``` -pip install didit-sdk + +## Webhooks + +`didit.webhooks.verify(raw_body, headers, secret)` checks the delivery and returns a typed event, or raises `WebhookVerificationError` with a machine-readable `reason` (`missing_signature`, `invalid_signature`, `missing_timestamp`, `timestamp_mismatch`, `timestamp_out_of_tolerance`, `invalid_payload`). + +- Pass the exact raw body bytes and the request headers (any mapping or list of pairs). +- `X-Signature` (raw body) and `X-Signature-V2` (canonical JSON, which survives middleware that re-encodes the body) are checked, and their `-Previous` variants during a secret rotation. `secret` can be a list: `verify(body, headers, [new_secret, old_secret])`. +- Deliveries older or newer than `tolerance` seconds (default 300) are refused. +- Anything the request carries (a malformed, huge or deeply nested body, odd headers) raises `WebhookVerificationError`, never another exception, so catching that one class is enough. A missing secret (`None`, for example an unset environment variable) or an empty one raises `ConfigurationError`. +- `X-Signature-Simple` (deprecated) counts only with `allow_simple=True`; it covers the envelope, not the body, so `event.body_authenticated` is then `False` and you should re-fetch the record from the API. +- `didit.webhooks.verify_parsed(payload, headers, secret)` is for frameworks that only give you the parsed JSON; it can check `X-Signature-V2` only. + +Events: `SessionStatusUpdatedEvent`, `SessionDataUpdatedEvent`, `UserStatusUpdatedEvent`, `UserDataUpdatedEvent`, `BusinessStatusUpdatedEvent`, `BusinessDataUpdatedEvent`, `ActivityCreatedEvent`, `TransactionCreatedEvent`, `TransactionStatusUpdatedEvent`, `TravelRuleStatusUpdatedEvent`, `WorkflowComplianceChangedEvent`, `KybRegistrySearchResolvedEvent`, and `UnknownEvent` (with the whole `payload`) for types this version does not know. `client.webhooks.verify(...)` does the same as `didit.webhooks.verify(...)`. + +## Authentication + +[Didit Authentication](https://docs.didit.me) gives your application its own OpenID Connect issuer for your end users (early access; production with the 2026-11-01 release). The `didit.authentication` module verifies the tokens that issuer signs, in your backend, and calls its token, session and step-up endpoints. It never uses the API key. Install it with `pip install "didit-sdk[auth]"`. + +```python +from didit.authentication import Authentication, TokenVerificationError + +# The publishable key (pk_live_... or pk_test_...) carries the issuer URL; +# Authentication("https://.auth.didit.me") works too. client_id is your application id. +auth = Authentication.from_publishable_key("pk_live_...", client_id="") + +try: + claims = auth.verify_access_token(access_token) +except TokenVerificationError as err: + ... # err.reason: "expired", "invalid_signature", "invalid_audience", ... +print(claims.sub, claims.sid, claims.vendor_data, claims.org) ``` -- API reference: https://docs.didit.me -- Design contract shared by every Didit server SDK: [docs/DESIGN.md](docs/DESIGN.md) +- `verify_access_token(token, audience=None)` accepts ES256 only (never `none`, HS* or RS*), requires the header `typ: at+jwt`, checks `iss`, `aud` (the given audience, else `client_id`), `exp`, `nbf` and `iat` with 60 s leeway, and the signature against the issuer's keys. Discovery and keys are fetched on first use and cached as the issuer says (5 minutes today); an unknown key id triggers at most one refetch per minute. After a failed fetch, verification that needs the keys raises `APIConnectionError` for a minute instead of contacting the issuer again (never `TokenVerificationError`, so an outage does not look like a bad token). The discovery document must name the configured issuer, keep the token endpoint on the issuer's origin and use https. To deny-list a token, key the list on its `jti` claim, not on the token string. +- `verify_id_token(token, nonce=..., audience=None)` does the same for ID tokens and checks the nonce of the sign-in (pass `nonce=None` for an ID token from a refresh, which has none). +- `create_pkce()`, `create_state()` and `create_nonce()` for the sign-in redirect; `exchange_code(code, code_verifier=..., redirect_uri=...)` and `refresh(refresh_token)` for tokens. Serialise refreshes for one session: reusing a consumed refresh token revokes the session. +- `get_session(access_token)` asks the issuer whether the session is still live (instant revocation check); `revoke_session(access_token)` signs the person out. +- `create_step_up(access_token, action="payout", amount="250.00", currency="EUR", return_to=...)` and `retrieve_step_up(access_token, step_up_id)`: go ahead without a step-up only when `step_up.required is False`, and after one only when `step_up.approved` is true. +- `AsyncAuthentication` has the same methods with `await`. One instance per process is the intended use. + +See [`examples/authentication.py`](examples/authentication.py). + +## Language notes + +Choices the shared design leaves to each language, made the Python way: + +- Method arguments: path ids are positional; body and query fields are keyword-only and default to `NOT_GIVEN`, so an omitted field is left out of the request and `None` is sent as JSON `null`. Each request body also has a `TypedDict` (`SessionsCreateParams`) for building it as a dict. +- Field names are the API's own. A name that is a Python keyword or would shadow a model method gets a trailing underscore in Python (`from_`), and keeps its wire name in `to_dict()`. A query parameter with the same name as a body field gets a `_query` suffix (`node_id_query`). +- Enums are open: `Literal["Approved", "Declined", ...] | str`, so a new value from the API never raises. +- Dates and date-times are `datetime.date` and `datetime.datetime` in responses, and accept either the object or an ISO 8601 string in requests. Decimal amounts stay strings, as the API sends them. +- Async list methods return an object you can `await` for the first page or `async for` over for every item. +- Operations that accept JSON or a file upload send multipart only when you pass a file. +- `api_key` is checked when a method needs it, so a client without one can still use `client.account` and verify webhooks. +- Types load on first use, so `import didit` stays fast even though the SDK has more than 800 types. + +## Supported runtimes + +CPython 3.10, 3.11, 3.12, 3.13 and 3.14 on Linux, macOS and Windows. Dependencies: `httpx`, `pydantic` 2 and `typing-extensions`; `cryptography` only with the `auth` extra. + +## Versioning + +Semantic versioning. Until 1.0.0 the version stays `0.x` and minor versions may change the public API. Releases are cut by release-please from conventional commits; see [CHANGELOG.md](CHANGELOG.md) once the first release is out. When an endpoint is removed from the API, or its method is renamed, the old method stays for one more minor version, marked deprecated (it emits a `DeprecationWarning` and type checkers flag it); a renamed method's old name keeps working as an alias of the new one. + +## Contributing and security + +See [CONTRIBUTING.md](CONTRIBUTING.md) (how the generated code is produced from `openapi/` and how to run the checks) and [SECURITY.md](SECURITY.md) for reporting vulnerabilities. [`docs/DESIGN.md`](docs/DESIGN.md) is the design contract shared by every Didit server SDK. ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a76f3ab --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Security policy + +## Reporting a vulnerability + +Please report vulnerabilities privately. Do not open a public issue, pull request or discussion. + +- GitHub private vulnerability reporting: open the **Security** tab of this repository and choose **Report a vulnerability**. +- Or email support@didit.me with "Security" in the subject. + +## What to include + +- The affected SDK version (`python -c "import didit; print(didit.__version__)"`) and Python version. +- A description of the issue and its impact. +- Steps or a minimal script to reproduce it. +- Any request id, log line or proof of concept that helps, with API keys, secrets, tokens and personal data removed. + +## What to expect + +We will acknowledge your report, keep you informed while we investigate, and tell you when a fix is released. +Please give us reasonable time to fix the issue before you disclose it publicly. +We are glad to credit you in the advisory if you wish. + +## Supported versions + +Security fixes go into the latest minor release of `didit-sdk`. +Upgrade to it to receive them. + +## Scope + +This policy covers this SDK (the code in this repository). +Vulnerabilities in the Didit API, the Didit dashboards or other Didit services are welcome too: report them to the same address, support@didit.me. diff --git a/codegen/didit_codegen/__init__.py b/codegen/didit_codegen/__init__.py new file mode 100644 index 0000000..12f374c --- /dev/null +++ b/codegen/didit_codegen/__init__.py @@ -0,0 +1 @@ +"""The generator of the Didit Python SDK's generated layer (docs/DESIGN.md section 5).""" diff --git a/codegen/didit_codegen/cli.py b/codegen/didit_codegen/cli.py new file mode 100644 index 0000000..21a42fb --- /dev/null +++ b/codegen/didit_codegen/cli.py @@ -0,0 +1,104 @@ +"""``python codegen/generate.py [--check]``: regenerate the SDK from ``openapi/``.""" + +from __future__ import annotations + +import argparse +import difflib +import filecmp +import pathlib +import shutil +import subprocess +import sys +import tempfile + +from .generate import GENERATED_ROOTS, generate +from .spec import CodegenError + +REPO = pathlib.Path(__file__).resolve().parents[2] + + +def _format(root: pathlib.Path) -> None: + ruff = shutil.which("ruff") + if ruff is None: + raise CodegenError("ruff is needed to format the generated code: run `uv sync` first.") + config = str(REPO / "pyproject.toml") + subprocess.run( + [ruff, "check", "--config", config, "--select", "I", "--fix", "--quiet", str(root)], + check=True, + ) + subprocess.run([ruff, "format", "--config", config, "--quiet", str(root)], check=True) + + +def write_tree(target: pathlib.Path) -> int: + files = generate(REPO / "openapi") + for rel, text in files.items(): + path = target / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + _format(target) + return len(files) + + +def _diff(expected: pathlib.Path, actual: pathlib.Path) -> list[str]: + problems: list[str] = [] + for root in GENERATED_ROOTS: + exp_root, act_root = expected / root, actual / root + exp = ( + {p.relative_to(exp_root) for p in exp_root.rglob("*") if p.is_file()} + if exp_root.exists() + else set() + ) + act = ( + { + p.relative_to(act_root) + for p in act_root.rglob("*") + if p.is_file() and "__pycache__" not in p.parts + } + if act_root.exists() + else set() + ) + for rel in sorted(exp - act): + problems.append(f"missing: {root}/{rel}") + for rel in sorted(act - exp): + problems.append(f"not generated (delete it): {root}/{rel}") + for rel in sorted(exp & act): + if not filecmp.cmp(exp_root / rel, act_root / rel, shallow=False): + problems.append(f"stale: {root}/{rel}") + if len(problems) < 5: + diff = difflib.unified_diff( + (act_root / rel).read_text("utf-8").splitlines(), + (exp_root / rel).read_text("utf-8").splitlines(), + "committed", + "generated", + lineterm="", + n=1, + ) + problems.extend(" " + line for line in list(diff)[:40]) + return problems + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--check", action="store_true", help="fail if the committed code is stale") + args = parser.parse_args(argv) + try: + with tempfile.TemporaryDirectory() as tmp: + out = pathlib.Path(tmp) + count = write_tree(out) + if args.check: + problems = _diff(out, REPO) + if problems: + print("Generated code is stale. Run `uv run python codegen/generate.py`.\n") + print("\n".join(problems[:200])) + return 1 + print(f"Generated code is up to date ({count} files).") + return 0 + for root in GENERATED_ROOTS: + shutil.rmtree(REPO / root, ignore_errors=True) + if (out / root).exists(): + shutil.copytree(out / root, REPO / root) + print(f"Wrote {count} generated files.") + return 0 + except CodegenError as exc: + print(f"codegen: {exc}", file=sys.stderr) + return 2 diff --git a/codegen/didit_codegen/generate.py b/codegen/didit_codegen/generate.py new file mode 100644 index 0000000..e84e8e8 --- /dev/null +++ b/codegen/didit_codegen/generate.py @@ -0,0 +1,96 @@ +"""Build every generated file from ``openapi/`` (pure: no I/O besides reading the specs).""" + +from __future__ import annotations + +import pathlib + +from .ops import Method, build_method +from .render import build_tree, render_resources, render_types +from .render_tests import render_tests +from .spec import Model, load +from .typegen import TypeBuilder, Usage, component_refs + +GENERATED_ROOTS = ("src/didit/types", "src/didit/resources", "tests/generated") + +# Attribute names of didit._models.BaseModel (and pydantic.BaseModel) that a +# response field must not shadow; such fields get a trailing underscore and an alias. +MODEL_RESERVED = frozenset( + { + "construct", + "construct_lenient", + "copy", + "dict", + "from_orm", + "json", + "parse_file", + "parse_obj", + "parse_raw", + "schema", + "schema_json", + "to_dict", + "to_json", + "update_forward_refs", + "validate", + "model_computed_fields", + "model_config", + "model_construct", + "model_copy", + "model_dump", + "model_dump_json", + "model_extra", + "model_fields", + "model_fields_set", + "model_json_schema", + "model_parametrized_name", + "model_post_init", + "model_rebuild", + "model_validate", + "model_validate_json", + "model_validate_strings", + # Builtins used in annotations, which a class attribute would shadow. + "bool", + "bytes", + "float", + "int", + "list", + "object", + "str", + } +) + + +def usage_of(model: Model) -> Usage: + usage = Usage() + for op in model.server_operations: + for code, raw in op.op.get("responses", {}).items(): + if str(code).startswith("2"): + component_refs(op.spec, op.spec.resolve(raw), usage.response) + body = op.op.get("requestBody") + if body: + body = op.spec.resolve(body) + for media in body.get("content", {}).values(): + schema = media.get("schema", {}) + # The body root becomes Params; only nested refs count. + component_refs(op.spec, op.spec.resolve(schema), usage.request) + return usage + + +def build(openapi_dir: pathlib.Path) -> tuple[Model, TypeBuilder, list[Method]]: + model = load(openapi_dir) + builder = TypeBuilder(model.specs, usage_of(model), MODEL_RESERVED) + methods = [build_method(op, builder) for op in model.server_operations] + return model, builder, methods + + +def generate(openapi_dir: pathlib.Path) -> dict[str, str]: + """Every generated file, keyed by its path relative to the repository root.""" + _, builder, methods = build(openapi_dir) + files: dict[str, str] = {} + for rel, text in render_types(builder.registry).items(): + files[f"src/didit/types/{rel}"] = text + roots = build_tree(methods) + for rel, text in render_resources(roots, builder.registry).items(): + files[f"src/didit/resources/{rel}"] = text + for rel, text in render_tests(methods, builder.registry).items(): + files[f"tests/generated/{rel}"] = text + return files diff --git a/codegen/didit_codegen/naming.py b/codegen/didit_codegen/naming.py new file mode 100644 index 0000000..0d06705 --- /dev/null +++ b/codegen/didit_codegen/naming.py @@ -0,0 +1,60 @@ +"""Name conversions for the generated code (docs/DESIGN.md sections 4 and 5.1).""" + +from __future__ import annotations + +import keyword +import re + +_WORD = re.compile(r"[A-Z]+(?=[A-Z][a-z0-9])|[A-Z]?[a-z0-9]+|[A-Z]+|[0-9]+") + +# Names the generated method signatures use for per-request options. +RESERVED_KWARGS = frozenset( + {"self", "extra_headers", "extra_query", "extra_body", "timeout", "idempotency_key"} +) + + +def words(text: str) -> list[str]: + return _WORD.findall(re.sub(r"[^A-Za-z0-9]+", " ", text)) + + +def _cap(word: str) -> str: + if word.isupper() and len(word) > 1: + return word[0] + word[1:].lower() + return word[:1].upper() + word[1:] + + +def pascal(text: str) -> str: + """``key_people_checks`` -> ``KeyPeopleChecks``; ``ID verification`` -> ``IdVerification``.""" + out = "".join(_cap(w) for w in words(text)) + if not out: + return "Value" + if out[0].isdigit(): + out = "V" + out + return out + + +def snake(text: str) -> str: + """``sessionId`` -> ``session_id``; ``Idempotency-Key`` -> ``idempotency_key``.""" + out = "_".join(w.lower() for w in words(text)) + if not out: + return "value" + if out[0].isdigit(): + out = "v_" + out + return out + + +def identifier(name: str, *, taken: frozenset[str] = frozenset()) -> str: + """A valid, non-keyword Python identifier for ``name``; ``from`` -> ``from_``.""" + out = name if name.isidentifier() and name.isascii() else snake(name) + if out.startswith("_"): + out = out.lstrip("_") or "value" + if not out.isidentifier(): + out = "v_" + out + while keyword.iskeyword(out) or out in taken: + out += "_" + return out + + +def resource_class(resource: str) -> str: + """``sessions.imports`` -> ``SessionsImports``.""" + return "".join(pascal(segment) for segment in resource.split(".")) diff --git a/codegen/didit_codegen/ops.py b/codegen/didit_codegen/ops.py new file mode 100644 index 0000000..e19cd44 --- /dev/null +++ b/codegen/didit_codegen/ops.py @@ -0,0 +1,452 @@ +"""Turn each server operation into the description of one SDK method.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Any, Literal + +from .naming import RESERVED_KWARGS, identifier, pascal, resource_class, snake +from .spec import CodegenError, Operation +from .typegen import Ann, TypeBuilder, TypeDef + +Json = Any + +BINARY_TYPES = ( + "application/pdf", + "text/csv", + "image/", + "application/octet-stream", + "application/zip", +) + + +@dataclass +class Arg: + py: str + wire: str + location: Literal["path", "query", "body"] + annotation: Ann + required: bool + description: str | None + schema: Json = None + example: Json = None + has_example: bool = False + + +@dataclass +class ResponseInfo: + kind: Literal["model", "bytes", "none", "page"] + annotation: Ann + cast_to: str + item: Ann | None = None + accept: str = "application/json" + status: int = 200 + example: Json = None + has_example: bool = False + schema: Json = None + content_type: str | None = None + + +@dataclass +class Method: + op: Operation + resource: str + name: str + path_args: list[Arg] + kwargs: list[Arg] + body_mode: Literal["none", "json", "multipart", "auto"] + file_fields: list[str] + idempotency: bool + response: ResponseInfo + params_type: str | None + body_example: Json = None + has_body_example: bool = False + whole_body: Arg | None = None + summary: str = "" + description: str = "" + deps: set[str] = field(default_factory=lambda: set()) + + @property + def is_async_def(self) -> bool: + return self.response.kind != "page" + + +def types_module(resource: str, method: str, kind: str) -> str: + return ".".join([*resource.split("."), f"{method}_{kind}"]) + + +def _example_of(media: Json, spec_schema: Json) -> tuple[bool, Json]: + if not isinstance(media, dict): + return False, None + if "example" in media: + return True, media["example"] + examples = media.get("examples") + if isinstance(examples, dict) and examples: + first = next(iter(examples.values())) + if isinstance(first, dict) and "value" in first: + return True, first["value"] + if isinstance(spec_schema, dict) and "example" in spec_schema: + return True, spec_schema["example"] + return False, None + + +def _first_text(op: Json, key: str) -> str: + text = op.get(key) + return text.strip() if isinstance(text, str) else "" + + +def _has_binary(spec_schema: Json, op: Operation) -> set[str]: + out: set[str] = set() + schema = op.spec.resolve(spec_schema) + if not isinstance(schema, dict): + return out + for name, prop in schema.get("properties", {}).items(): + prop = op.spec.resolve(prop) + if prop.get("format") == "binary" or ( + prop.get("type") == "array" + and op.spec.resolve(prop.get("items", {})).get("format") == "binary" + ): + out.add(name) + return out + + +def _merge_body(op: Operation, content: dict[str, Json]) -> tuple[Json, str, list[str], Json]: + """The schema of the method's body arguments, the body mode, the file fields and the media.""" + json_media = content.get("application/json") + multipart_media = content.get("multipart/form-data") + if json_media is None and multipart_media is None: + form = content.get("application/x-www-form-urlencoded") + if form is None: + raise CodegenError(f"{op.key}: unsupported request content types {sorted(content)}.") + json_media = form + if json_media is not None and multipart_media is None: + return json_media.get("schema", {}), "json", [], json_media + if json_media is None: + assert multipart_media is not None + schema = multipart_media.get("schema", {}) + return schema, "multipart", sorted(_has_binary(schema, op)), multipart_media + assert multipart_media is not None + json_schema = op.spec.resolve(json_media.get("schema", {})) + multipart_schema = op.spec.resolve(multipart_media.get("schema", {})) + files = _has_binary(multipart_schema, op) - set(json_schema.get("properties", {})) + if not files: + return json_media.get("schema", {}), "json", [], json_media + merged = dict(json_schema) + props = dict(json_schema.get("properties", {})) + for name, prop in multipart_schema.get("properties", {}).items(): + props.setdefault(name, prop) + merged["properties"] = props + # Either encoding works, so only a field both of them require is required. + multipart_required = set(multipart_schema.get("required", [])) + merged["required"] = [r for r in json_schema.get("required", []) if r in multipart_required] + merged.pop("$ref", None) + return merged, "auto", sorted(files), json_media + + +def _all_fields(builder: TypeBuilder, td: TypeDef) -> list[Any]: + out: list[Any] = [] + for base in td.bases: + out.extend(_all_fields(builder, builder.registry.defs[base])) + names = {f.wire for f in td.fields} + out = [f for f in out if f.wire not in names] + out.extend(td.fields) + return out + + +def _path_names(path: str) -> list[str]: + return re.findall(r"\{([^}]+)\}", path) + + +def build_method(op: Operation, builder: TypeBuilder) -> Method: + sdk = op.sdk + spec = op.spec + rc = resource_class(sdk.resource) + pascal(sdk.method) + params_name = rc + "Params" + params_module = types_module(sdk.resource, sdk.method, "params") + + # Path arguments, in path order. + path_params = {p["name"]: p for p in op.parameters if p["in"] == "path"} + path_args: list[Arg] = [] + used: set[str] = set(RESERVED_KWARGS) + for name in _path_names(op.path): + if sdk.scope == "application" and name in ("organization_id", "application_id"): + continue + param = path_params.get(name, {"name": name, "schema": {"type": "string"}}) + py = identifier(snake(name), taken=frozenset(used)) + used.add(py) + schema = param.get("schema", {"type": "string"}) + has_ex = "example" in param or (isinstance(schema, dict) and "example" in schema) + path_args.append( + Arg( + py=py, + wire=name, + location="path", + annotation=Ann("str"), + required=True, + description=_first_text(param, "description") or None, + schema=schema, + example=param.get( + "example", schema.get("example") if isinstance(schema, dict) else None + ), + has_example=has_ex, + ) + ) + + # Body arguments. + kwargs: list[Arg] = [] + body_mode: Literal["none", "json", "multipart", "auto"] = "none" + file_fields: list[str] = [] + params_type: str | None = None + body_example: Json = None + has_body_example = False + whole_body: Arg | None = None + request_body = spec.resolve(op.op.get("requestBody")) if op.op.get("requestBody") else None + if request_body: + schema, mode, file_fields, media = _merge_body(op, request_body.get("content", {})) + body_mode = mode # type: ignore[assignment] + has_body_example, body_example = _example_of(media, spec.resolve(schema)) + root = spec.resolve(schema) + if isinstance(root, dict) and "allOf" in root: + props, required, _ = builder._collect(spec, root) + root = { + "type": "object", + "description": root.get("description"), + "properties": props, + "required": sorted(required), + } + ann = builder.param(spec, root, params_name, params_module) + td = builder.registry.defs.get(ann.expr) + if td is not None and td.kind == "typeddict" and ann.expr == params_name: + params_type = params_name + for f in _all_fields(builder, td): + py = identifier(f.wire, taken=frozenset(used)) + used.add(py) + prop_schema = _prop_schema(spec, root, f.wire) + kwargs.append( + Arg( + py=py, + wire=f.wire, + location="body", + annotation=f.annotation, + required=f.required, + description=f.description, + schema=prop_schema, + ) + ) + else: + whole_body = Arg( + py=identifier("body", taken=frozenset(used)), + wire="", + location="body", + annotation=ann, + required=bool(request_body.get("required")), + description=_first_text(request_body, "description") or None, + schema=root, + ) + used.add(whole_body.py) + + # Query arguments. + for param in op.parameters: + if param["in"] != "query": + continue + name = param["name"] + base = snake(name) if not name.isidentifier() else name + if base in used: + base = f"{base}_query" + py = identifier(base, taken=frozenset(used)) + used.add(py) + schema = param.get("schema", {}) + ann = builder.param(spec, schema, params_name + pascal(name), params_module) + has_ex = "example" in param or (isinstance(schema, dict) and "example" in schema) + kwargs.append( + Arg( + py=py, + wire=name, + location="query", + annotation=ann, + required=bool(param.get("required")), + description=_first_text(param, "description") or None, + schema=schema, + example=param.get( + "example", schema.get("example") if isinstance(schema, dict) else None + ), + has_example=has_ex, + ) + ) + + idempotency = any( + p["in"] == "header" and p["name"].lower() == "idempotency-key" for p in op.parameters + ) + + response = _response(op, builder, rc) + method = Method( + op=op, + resource=sdk.resource, + name=sdk.method, + path_args=path_args, + kwargs=sorted(kwargs, key=lambda a: not a.required), + body_mode=body_mode, + file_fields=file_fields, + idempotency=idempotency, + response=response, + params_type=params_type, + body_example=body_example, + has_body_example=has_body_example, + whole_body=whole_body, + summary=_first_text(op.op, "summary"), + description=_first_text(op.op, "description"), + ) + deps: set[str] = set() + for arg in [*method.kwargs, *([whole_body] if whole_body else [])]: + deps |= set(arg.annotation.deps) + deps |= set(response.annotation.deps) + if response.item is not None: + deps |= set(response.item.deps) + method.deps = deps + return method + + +def _prop_schema(spec: Any, root: Json, wire: str) -> Json: + props: dict[str, Json] = {} + stack = [root] + while stack: + cur = spec.resolve(stack.pop()) + if not isinstance(cur, dict): + continue + props.update({k: v for k, v in cur.get("properties", {}).items() if k not in props}) + stack.extend(cur.get("allOf", [])) + return props.get(wire, {}) + + +def _response(op: Operation, builder: TypeBuilder, rc: str) -> ResponseInfo: + spec = op.spec + successes: list[tuple[int, Json]] = [] + for code, raw in op.op.get("responses", {}).items(): + if not str(code).startswith("2"): + continue + successes.append((int(code), spec.resolve(raw))) + successes.sort(key=lambda x: x[0]) + json_responses: list[tuple[int, Json]] = [] + binary: tuple[int, str] | None = None + empty = False + for code, resp in successes: + content = resp.get("content") or {} + media = content.get("application/json") + if media is not None and "schema" in media: + json_responses.append((code, media)) + continue + if media is not None: + json_responses.append((code, {"schema": {}, **media})) + continue + other = [ct for ct in content if any(ct.startswith(b) for b in BINARY_TYPES)] + if other: + binary = (code, other[0]) + elif not content and code != 204 and op.http_method not in ("delete", "head"): + # A body is promised but not described: return the JSON as is. + json_responses.append((code, {"schema": {}})) + else: + empty = True + module = types_module(op.sdk.resource, op.sdk.method, "response") + if json_responses: + anns: list[tuple[int, Ann]] = [] + seen_schemas: list[tuple[Json, Ann]] = [] + for index, (code, media) in enumerate(json_responses): + schema = media.get("schema", {}) + reuse = next((a for s, a in seen_schemas if s == schema), None) + if reuse is not None: + anns.append((code, reuse)) + continue + hint = rc + "Response" + ("" if index == 0 else str(code)) + resolved = spec.resolve(schema) + if ( + isinstance(resolved, dict) + and resolved.get("type") == "array" + and "$ref" not in schema + ): + item = builder.response(spec, resolved.get("items", {}), hint + "Item", module) + ann = Ann(f"list[{item.expr}]", item.deps) + if resolved.get("nullable"): + ann = ann.optional() + else: + ann = builder.response(spec, schema, hint, module) + seen_schemas.append((schema, ann)) + anns.append((code, ann)) + code, media = json_responses[0] + has_ex, example = _example_of(media, spec.resolve(media.get("schema", {}))) + distinct: list[Ann] = [] + for _, a in anns: + if a not in distinct: + distinct.append(a) + if len(distinct) == 1: + annotation = distinct[0] + cast_to = _runtime_expr(annotation) + else: + parts = " | ".join(a.expr for a in distinct) + deps: set[str] = set() + for a in distinct: + deps |= set(a.deps) + annotation = Ann(parts, frozenset(deps)) + cast_to = "{" + ", ".join(f"{c}: {_runtime_expr(a)}" for c, a in anns) + "}" + if empty: + annotation = annotation.optional() + page_item = _page_item(op, builder, annotation) + if page_item is not None: + return ResponseInfo( + kind="page", + annotation=annotation, + cast_to=cast_to, + item=page_item, + status=code, + example=example, + has_example=has_ex, + schema=media.get("schema", {}), + content_type="application/json", + ) + return ResponseInfo( + kind="model", + annotation=annotation, + cast_to=cast_to, + status=code, + example=example, + has_example=has_ex, + schema=media.get("schema", {}), + content_type="application/json", + ) + if binary is not None: + code, content_type = binary + return ResponseInfo( + kind="bytes", + annotation=Ann("bytes"), + cast_to="bytes", + accept=f"{content_type}, application/json", + status=code, + content_type=content_type, + ) + code = successes[0][0] if successes else 204 + return ResponseInfo(kind="none", annotation=Ann("None"), cast_to="None", status=code) + + +def _runtime_expr(ann: Ann) -> str: + return ann.expr + + +def _page_item(op: Operation, builder: TypeBuilder, annotation: Ann) -> Ann | None: + if op.http_method != "get": + return None + query = {p["name"] for p in op.parameters if p["in"] == "query"} + if not {"limit", "offset"} <= query: + return None + td = builder.registry.defs.get(annotation.expr) + if td is None or td.kind != "model": + return None + fields = {f.wire: f for f in _all_fields(builder, td)} + if not {"count", "next", "previous", "results"} <= set(fields): + return None + results = fields["results"].annotation.expr + if results.endswith(" | None"): + results = results[: -len(" | None")] + if not results.startswith("list[") or not results.endswith("]"): + return None + inner = results[len("list[") : -1] + return Ann(inner, fields["results"].annotation.deps) diff --git a/codegen/didit_codegen/render.py b/codegen/didit_codegen/render.py new file mode 100644 index 0000000..c380bf0 --- /dev/null +++ b/codegen/didit_codegen/render.py @@ -0,0 +1,664 @@ +"""Write the generated layer: ``src/didit/types``, ``src/didit/resources`` and ``tests/generated``.""" + +from __future__ import annotations + +import json +import re +from collections import defaultdict + +from .naming import resource_class +from .ops import Method +from .typegen import ( + DATE, + DATETIME, + FILE_TYPES, + LITERAL, + MAPPING, + SEQUENCE, + Registry, + TypeDef, +) + +HEADER = "# Code generated by codegen from openapi/*.json. DO NOT EDIT.\n" + +# Hand-written classes mixed into generated resources. +RESOURCE_MIXINS: dict[str, tuple[str, str]] = { + "webhooks": ("didit.webhooks._mixin", "WebhooksMixin"), +} + + +def plain_text(text: str) -> str: + """Upstream descriptions as written in this repository: no em dashes.""" + return text.replace(" \u2014 ", " - ").replace("\u2014", " - ") + + +def docstring(text: str | None, indent: str) -> str: + if not text: + return "" + body = plain_text(text).replace("\\", "\\\\").replace('"""', '\\"\\"\\"').strip() + if body.endswith('"'): + body += " " + lines = body.splitlines() + if len(lines) == 1: + return f'{indent}"""{lines[0]}"""\n' + out = [f'{indent}"""{lines[0]}'] + for line in lines[1:]: + out.append(f"{indent}{line}".rstrip()) + out.append(f'{indent}"""') + return "\n".join(out) + "\n" + + +def module_path(module: str) -> str: + return module.replace(".", "/") + ".py" + + +def _topo(names: list[str], registry: Registry) -> list[str]: + local = set(names) + order: list[str] = [] + state: dict[str, int] = {} + + def visit(name: str) -> None: + if state.get(name) == 2: + return + if state.get(name) == 1: + raise RuntimeError(f"Cycle between generated types at {name}.") + state[name] = 1 + for dep in sorted(registry.defs[name].deps): + if dep in local: + visit(dep) + state[name] = 2 + order.append(name) + + for name in names: + visit(name) + return order + + +# Types -------------------------------------------------------------------------- + + +def render_types(registry: Registry) -> dict[str, str]: + files: dict[str, str] = {} + packages: set[str] = set() + for module, names in registry.modules.items(): + parts = module.split(".") + for i in range(1, len(parts)): + packages.add(".".join(parts[:i])) + files[module_path(module)] = _render_type_module(module, names, registry) + for package in packages: + files[package.replace(".", "/") + "/__init__.py"] = HEADER + files["__init__.py"] = _render_types_init(registry) + return files + + +def _imports_for(defs: list[TypeDef], registry: Registry, module: str) -> list[str]: + deps: set[str] = set() + kinds = {td.kind for td in defs} + uses_field = False + for td in defs: + deps |= td.deps + for f in td.fields: + if td.kind == "model" and f.name != f.wire: + uses_field = True + lines: list[str] = ["from __future__ import annotations", ""] + std: list[str] = [] + if DATETIME in deps or DATE in deps: + std.append("import datetime as _dt") + typing_names = sorted( + n + for n, marker in (("Literal", LITERAL), ("Mapping", MAPPING), ("Sequence", SEQUENCE)) + if marker in deps + ) + if typing_names: + std.append(f"from typing import {', '.join(typing_names)}") + if std: + lines.extend(std) + lines.append("") + third: list[str] = [] + if uses_field: + third.append("from pydantic import Field") + te: list[str] = [] + if "typeddict" in kinds: + if any(f.required for td in defs if td.kind == "typeddict" for f in td.fields): + te.append("Required") + te.append("TypedDict") + if "alias" in kinds: + te.append("TypeAlias") + if te: + third.append(f"from typing_extensions import {', '.join(sorted(te))}") + if third: + lines.extend(third) + lines.append("") + first: list[str] = [] + if any(td.kind == "model" and not td.bases for td in defs): + first.append("from didit._models import BaseModel") + if FILE_TYPES in deps: + first.append("from didit._types import FileTypes") + local = {td.name for td in defs} + by_module: dict[str, list[str]] = defaultdict(list) + for dep in sorted(deps): + if dep.startswith("@") or dep in local: + continue + target = registry.defs[dep] + by_module[target.module].append(dep) + for mod in sorted(by_module): + first.append(f"from didit.types.{mod} import {', '.join(sorted(by_module[mod]))}") + if first: + lines.extend(first) + lines.append("") + return lines + + +def _render_type_module(module: str, names: list[str], registry: Registry) -> str: + ordered = _topo(names, registry) + defs = [registry.defs[n] for n in ordered] + out = [HEADER] + out.extend(line + "\n" for line in _imports_for(defs, registry, module)) + out.append("__all__ = [" + ", ".join(json.dumps(n) for n in sorted(names)) + "]\n\n") + for td in defs: + out.append("\n") + out.append(_render_def(td)) + return "".join(out) + + +def _field_doc(description: str | None, deprecated: bool) -> str | None: + text = description or "" + if deprecated: + text = ("Deprecated. " + text).strip() + return text or None + + +def _render_def(td: TypeDef) -> str: + if td.kind == "alias": + assert td.alias is not None + text = f"{td.name}: TypeAlias = {td.alias.expr}\n" + doc = docstring(td.description, "") + return text + doc + if td.kind == "model": + bases = ", ".join(td.bases) or "BaseModel" + out = [f"class {td.name}({bases}):\n"] + out.append(docstring(td.description, " ") or "") + if not td.fields and not td.description: + out.append(" pass\n") + for f in td.fields: + ann = f.annotation.expr + if f.name != f.wire: + default = "default=None, " if not f.required else "" + line = f" {f.name}: {ann} = Field({default}alias={json.dumps(f.wire, ensure_ascii=False)})\n" + elif not f.required: + line = f" {f.name}: {ann} = None\n" + else: + line = f" {f.name}: {ann}\n" + out.append("\n" if out[-1] != f"class {td.name}({bases}):\n" else "") + out.append(line) + out.append(docstring(_field_doc(f.description, f.deprecated), " ")) + return "".join(out) + "\n" + # TypedDict + functional = any( + not f.wire.isidentifier() or not f.wire.isascii() or _is_keyword(f.wire) for f in td.fields + ) + if functional: + if td.bases: + raise RuntimeError(f"{td.name}: a functional TypedDict cannot have bases.") + items = [] + for f in td.fields: + ann = f"Required[{f.annotation.expr}]" if f.required else f.annotation.expr + items.append(f" {json.dumps(f.wire, ensure_ascii=False)}: {ann},\n") + text = ( + f'{td.name} = TypedDict(\n "{td.name}",\n {{\n' + + "".join(items) + + " },\n total=False,\n)\n" + ) + return text + docstring(td.description, "") + bases = ", ".join(td.bases) if td.bases else "TypedDict" + total = ", total=False" + out = [f"class {td.name}({bases}{total}):\n"] + out.append(docstring(td.description, " ") or "") + if not td.fields and not td.description: + out.append(" pass\n") + for f in td.fields: + ann = f"Required[{f.annotation.expr}]" if f.required else f.annotation.expr + if len(out) > 1: + out.append("\n") + out.append(f" {f.wire}: {ann}\n") + out.append(docstring(_field_doc(f.description, f.deprecated), " ")) + return "".join(out) + "\n" + + +def _is_keyword(name: str) -> bool: + import keyword + + return keyword.iskeyword(name) + + +def _render_types_init(registry: Registry) -> str: + names = sorted(registry.defs) + out = [HEADER, '"""Every request and response type of the Didit API.\n\n'] + out.append("Names follow docs/DESIGN.md section 5.1. Modules load on first use, so\n") + out.append('``import didit`` stays fast.\n"""\n\n') + out.append("from __future__ import annotations\n\n") + out.append("import importlib\nfrom typing import TYPE_CHECKING, Any\n\n") + out.append("if TYPE_CHECKING:\n") + by_module: dict[str, list[str]] = defaultdict(list) + for name in names: + by_module[registry.defs[name].module].append(name) + for mod in sorted(by_module): + imported = ", ".join(f"{n} as {n}" for n in sorted(by_module[mod])) + out.append(f" from didit.types.{mod} import {imported}\n") + out.append("\n_MODULES: dict[str, str] = {\n") + for name in names: + out.append(f' "{name}": "{registry.defs[name].module}",\n') + out.append("}\n\n") + out.append("__all__ = [\n" + "".join(f' "{n}",\n' for n in names) + "]\n\n\n") + out.append( + "def __getattr__(name: str) -> Any:\n" + " module = _MODULES.get(name)\n" + " if module is None:\n" + ' raise AttributeError(f"module {__name__!r} has no attribute {name!r}")\n' + ' value = getattr(importlib.import_module(f"didit.types.{module}"), name)\n' + " globals()[name] = value\n" + " return value\n\n\n" + "def __dir__() -> list[str]:\n" + " return __all__\n" + ) + return "".join(out) + + +# Resources ------------------------------------------------------------------------ + + +class ResourceNode: + def __init__(self, path: str) -> None: + self.path = path + self.methods: list[Method] = [] + self.children: dict[str, ResourceNode] = {} + + @property + def cls(self) -> str: + return resource_class(self.path) + + @property + def module(self) -> str: + parts = self.path.split(".") + if self.children: + return "didit.resources." + ".".join(parts) + return "didit.resources." + ".".join(parts) + + @property + def file(self) -> str: + parts = self.path.split(".") + if self.children: + return "/".join(parts) + "/__init__.py" + return "/".join(parts) + ".py" + + +def build_tree(methods: list[Method]) -> dict[str, ResourceNode]: + roots: dict[str, ResourceNode] = {} + nodes: dict[str, ResourceNode] = {} + for method in methods: + parts = method.resource.split(".") + for i in range(1, len(parts) + 1): + path = ".".join(parts[:i]) + if path not in nodes: + nodes[path] = ResourceNode(path) + if i == 1: + roots[path] = nodes[path] + else: + nodes[".".join(parts[: i - 1])].children[parts[i - 1]] = nodes[path] + nodes[method.resource].methods.append(method) + return roots + + +def all_nodes(roots: dict[str, ResourceNode]) -> list[ResourceNode]: + out: list[ResourceNode] = [] + stack = list(roots.values()) + while stack: + node = stack.pop() + out.append(node) + stack.extend(node.children.values()) + return sorted(out, key=lambda n: n.path) + + +def render_resources(roots: dict[str, ResourceNode], registry: Registry) -> dict[str, str]: + files: dict[str, str] = {} + for node in all_nodes(roots): + files[node.file] = _render_resource(node, registry) + files["__init__.py"] = _render_root(roots) + return files + + +_SHADOWED = re.compile(r"(? str: + """Resource classes have methods named ``list``, so annotations there spell + the builtins as ``builtins.list``.""" + return _SHADOWED.sub(lambda m: f"builtins.{m.group(1)}[", expr) + + +def _sig_args(method: Method) -> list[str]: + args = ["self"] + for arg in method.path_args: + args.append(f"{arg.py}: str") + args.append("*") + if method.whole_body is not None: + wb = method.whole_body + if wb.required: + args.append(f"{wb.py}: {builtin_safe(wb.annotation.expr)}") + else: + args.append(f"{wb.py}: {builtin_safe(wb.annotation.expr)} | NotGiven = NOT_GIVEN") + for arg in method.kwargs: + if arg.required: + args.append(f"{arg.py}: {builtin_safe(arg.annotation.expr)}") + else: + args.append(f"{arg.py}: {builtin_safe(arg.annotation.expr)} | NotGiven = NOT_GIVEN") + if method.idempotency: + args.append("idempotency_key: str | NotGiven = NOT_GIVEN") + args.extend( + [ + "extra_headers: Headers | None = None", + "extra_query: Query | None = None", + "extra_body: Body | None = None", + "timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN", + ] + ) + return args + + +def _method_doc(method: Method) -> str: + parts: list[str] = [] + if method.op.deprecated: + parts.append( + "Deprecated: " + + (method.op.deprecation_note or "this operation is deprecated in the API contract.") + ) + if method.summary: + parts.append(method.summary.rstrip(".") + ".") + if method.description and method.description != method.summary: + parts.append(method.description) + parts.append(f"``{method.op.http_method.upper()} {method.op.path}``") + arg_lines: list[str] = [] + for arg in [ + *method.path_args, + *([method.whole_body] if method.whole_body else []), + *method.kwargs, + ]: + desc = (arg.description or "").strip() + desc = " ".join(desc.split()) + arg_lines.append(f" {arg.py}: {desc}" if desc else f" {arg.py}") + if method.idempotency: + arg_lines.append( + " idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key " + "returns the first result instead of acting twice." + ) + arg_lines.extend( + [ + " extra_headers: Headers to add to (or, with ``None``, remove from) this request.", + " extra_query: Query parameters to add to this request.", + " extra_body: Top-level JSON fields to add to this request's body.", + " timeout: Overrides the client's timeout for this request, in seconds.", + ] + ) + parts.append("Args:\n" + "\n".join(arg_lines)) + return "\n\n".join(parts) + + +def _call(method: Method, is_async: bool) -> str: + op = method.op + lines: list[str] = [] + page = method.response.kind == "page" + fn = "request_page" if page else "request" + lines.append(f'"{op.http_method}"') + lines.append(json.dumps(op.path)) + lines.append(f"cast_to={builtin_safe(method.response.cast_to)}") + if method.response.kind == "page": + assert method.response.item is not None + lines.append(f"item_type={builtin_safe(method.response.item.expr)}") + if method.path_args: + pp = ", ".join(f"{json.dumps(a.wire)}: {a.py}" for a in method.path_args) + lines.append(f"path_params={{{pp}}}") + if op.sdk.scope: + lines.append(f'scope="{op.sdk.scope}"') + lines.append(f'auth="{op.sdk.auth}"') + if op.api != "verification": + lines.append(f'api="{op.api}"') + query = [a for a in method.kwargs if a.location == "query"] + if query: + q = ", ".join(f"{json.dumps(a.wire)}: {a.py}" for a in query) + lines.append(f"query={{{q}}}") + body = [a for a in method.kwargs if a.location == "body"] + if method.whole_body is not None: + lines.append(f"body={method.whole_body.py}") + elif method.body_mode != "none": + b = ", ".join(f"{json.dumps(a.wire)}: {a.py}" for a in body) + lines.append(f"body={{{b}}}") + if method.body_mode == "multipart": + lines.append("multipart=True") + elif method.body_mode == "auto": + lines.append('multipart="auto"') + if method.file_fields: + ff = ", ".join(json.dumps(f) for f in method.file_fields) + lines.append(f"file_fields=({ff},)") + if method.idempotency: + lines.append('headers={"Idempotency-Key": idempotency_key}') + if method.response.accept != "application/json": + lines.append(f"accept={json.dumps(method.response.accept)}") + lines.extend( + [ + "extra_headers=extra_headers", + "extra_query=extra_query", + "extra_body=extra_body", + "timeout=timeout", + ] + ) + inner = ",\n ".join(lines) + awaited = "await " if is_async and not page else "" + return_type = _return_type(method, is_async) + return ( + f" return cast(\n" + f' "{return_type}",\n' + f" {awaited}self._client.{fn}(\n" + f" {inner},\n" + f" ),\n" + f" )\n" + ) + + +def _return_type(method: Method, is_async: bool) -> str: + if method.response.kind == "page": + assert method.response.item is not None + wrapper = "AsyncPaginator" if is_async else "SyncPage" + return f"{wrapper}[{builtin_safe(method.response.item.expr)}]" + return builtin_safe(method.response.annotation.expr) + + +def _render_method(method: Method, is_async: bool) -> str: + page = method.response.kind == "page" + kw = "async def" if is_async and not page else "def" + out: list[str] = [] + if method.op.deprecated: + note = method.op.deprecation_note or "This operation is deprecated in the API contract." + out.append(f" @deprecated({json.dumps(note)})\n") + args = ",\n ".join(_sig_args(method)) + out.append( + f" {kw} {method.name}(\n {args},\n ) -> {_return_type(method, is_async)}:\n" + ) + out.append(docstring(_method_doc(method), " ")) + out.append(_call(method, is_async)) + return "".join(out) + + +def _render_resource(node: ResourceNode, registry: Registry) -> str: + deps: set[str] = set() + for method in node.methods: + deps |= method.deps + has_page = any(m.response.kind == "page" for m in node.methods) + has_deprecated = any(m.op.deprecated for m in node.methods) + out: list[str] = [HEADER] + doc = f"The ``{node.path}`` resource." + out.append(f'"""{doc}"""\n\n') + out.append("from __future__ import annotations\n\n") + std = [] + if deps & {DATETIME, DATE}: + std.append("import datetime as _dt") + std.append("from functools import cached_property") + has_methods = bool(node.methods) + typing_names = (["TYPE_CHECKING"] if node.children else []) + (["cast"] if has_methods else []) + typing_names += [ + n + for n, m in (("Literal", LITERAL), ("Mapping", MAPPING), ("Sequence", SEQUENCE)) + if m in deps + ] + if typing_names: + std.append( + f"from typing import {', '.join(sorted(typing_names, key=lambda s: (s != 'TYPE_CHECKING', s.lower())))}" + ) + out.append("\n".join(std) + "\n\n") + if has_methods: + out.append("import httpx\n") + if has_deprecated: + out.append("from typing_extensions import deprecated\n") + if has_methods or has_deprecated: + out.append("\n") + first = [] + if has_page: + first.append("from didit._pagination import AsyncPaginator, SyncPage") + first.append("from didit._resource import AsyncAPIResource, SyncAPIResource") + if has_methods: + first.append( + "from didit._response import (\n" + " async_to_raw_response_wrapper,\n" + " async_to_streamed_response_wrapper,\n" + " to_raw_response_wrapper,\n" + " to_streamed_response_wrapper,\n" + ")" + ) + types_names = ["NOT_GIVEN", "Body", "Headers", "NotGiven", "Query"] + if FILE_TYPES in deps: + types_names.append("FileTypes") + first.append( + f"from didit._types import {', '.join(sorted(types_names, key=lambda s: (not s.isupper(), s)))}" + ) + mixin = RESOURCE_MIXINS.get(node.path) + if mixin: + first.append(f"from {mixin[0]} import {mixin[1]}") + by_module: dict[str, list[str]] = defaultdict(list) + for dep in sorted(deps): + if dep.startswith("@"): + continue + by_module[registry.defs[dep].module].append(dep) + for mod in sorted(by_module): + first.append(f"from didit.types.{mod} import {', '.join(sorted(by_module[mod]))}") + out.append("\n".join(first) + "\n\n") + children = sorted(node.children.items()) + if children: + out.append("if TYPE_CHECKING:\n") + for _, child in children: + c = child.cls + out.append( + f" from {child.module} import (\n" + f" Async{c},\n" + f" Async{c}WithRawResponse,\n" + f" Async{c}WithStreamingResponse,\n" + f" {c},\n" + f" {c}WithRawResponse,\n" + f" {c}WithStreamingResponse,\n" + f" )\n" + ) + out.append("\n") + cls = node.cls + all_names = [cls, f"Async{cls}", f"{cls}WithRawResponse", f"Async{cls}WithRawResponse", + f"{cls}WithStreamingResponse", f"Async{cls}WithStreamingResponse"] # fmt: skip + out.append("__all__ = [" + ", ".join(json.dumps(n) for n in all_names) + "]\n\n") + + for is_async in (False, True): + prefix = "Async" if is_async else "" + base = "AsyncAPIResource" if is_async else "SyncAPIResource" + bases = f"{mixin[1]}, {base}" if mixin else base + out.append(f"\nclass {prefix}{cls}({bases}):\n") + out.append(docstring(_resource_doc(node), " ")) + for name, child in children: + out.append("\n @cached_property\n") + out.append(f" def {name}(self) -> {prefix}{child.cls}:\n") + out.append(f" from {child.module} import {prefix}{child.cls}\n\n") + out.append(f" return {prefix}{child.cls}(self._client)\n") + for kind in ("RawResponse", "StreamingResponse"): + attr = "with_raw_response" if kind == "RawResponse" else "with_streaming_response" + out.append("\n @cached_property\n") + out.append(f" def {attr}(self) -> {prefix}{cls}With{kind}:\n") + what = ( + "the raw HTTP response" + if kind == "RawResponse" + else "the HTTP response before reading its body" + ) + out.append(f' """Every method of this resource, returning {what}."""\n') + out.append(f" return {prefix}{cls}With{kind}(self)\n") + for method in node.methods: + out.append("\n") + out.append(_render_method(method, is_async)) + out.append("\n") + + for kind in ("RawResponse", "StreamingResponse"): + for is_async in (False, True): + prefix = "Async" if is_async else "" + wrapper = { + ("RawResponse", False): "to_raw_response_wrapper", + ("RawResponse", True): "async_to_raw_response_wrapper", + ("StreamingResponse", False): "to_streamed_response_wrapper", + ("StreamingResponse", True): "async_to_streamed_response_wrapper", + }[(kind, is_async)] + out.append(f"\nclass {prefix}{cls}With{kind}:\n") + out.append(f" def __init__(self, resource: {prefix}{cls}) -> None:\n") + out.append(" self._resource = resource\n") + for method in node.methods: + out.append(f" self.{method.name} = {wrapper}(resource.{method.name})\n") + for name, child in children: + out.append("\n @cached_property\n") + out.append(f" def {name}(self) -> {prefix}{child.cls}With{kind}:\n") + out.append(f" from {child.module} import {prefix}{child.cls}With{kind}\n\n") + out.append(f" return {prefix}{child.cls}With{kind}(self._resource.{name})\n") + out.append("\n") + text = "".join(out) + if "builtins." in text: + text = text.replace( + "from __future__ import annotations\n\n", + "from __future__ import annotations\n\nimport builtins\n", + 1, + ) + return text + + +def _resource_doc(node: ResourceNode) -> str: + ops = sorted(m.name for m in node.methods) + subs = sorted(node.children) + text = f"The ``{node.path}`` resource of the Didit API." + if ops: + text += "\n\nMethods: " + ", ".join(f"``{m}``" for m in ops) + "." + if subs: + text += "\n\nNested resources: " + ", ".join(f"``{s}``" for s in subs) + "." + return text + + +def _render_root(roots: dict[str, ResourceNode]) -> str: + out = [HEADER, '"""The resource tree of the Didit API (docs/DESIGN.md section 4)."""\n\n'] + out.append("from __future__ import annotations\n\n") + out.append("from functools import cached_property\nfrom typing import TYPE_CHECKING\n\n") + out.append("from didit._base_client import AsyncAPIClient, SyncAPIClient\n\n") + out.append("if TYPE_CHECKING:\n") + for node in sorted(roots.values(), key=lambda n: n.path): + out.append(f" from {node.module} import Async{node.cls}, {node.cls}\n") + out.append('\n__all__ = ["AsyncRootResources", "SyncRootResources"]\n\n') + for is_async in (False, True): + prefix = "Async" if is_async else "" + base = "AsyncAPIClient" if is_async else "SyncAPIClient" + out.append( + f"\nclass {prefix}{'RootResources' if is_async else 'SyncRootResources'}({base}):\n" + ) + out.append(' """Top-level resources, loaded on first use."""\n') + for name, node in sorted(roots.items()): + out.append("\n @cached_property\n") + out.append(f" def {name}(self) -> {prefix}{node.cls}:\n") + out.append(f" from {node.module} import {prefix}{node.cls}\n\n") + out.append(f" return {prefix}{node.cls}(self)\n") + out.append("\n") + return "".join(out) diff --git a/codegen/didit_codegen/render_tests.py b/codegen/didit_codegen/render_tests.py new file mode 100644 index 0000000..f79dee5 --- /dev/null +++ b/codegen/didit_codegen/render_tests.py @@ -0,0 +1,506 @@ +"""One generated test per server operation (docs/DESIGN.md section 11). + +Each test calls the SDK method with values from the spec's examples (or values +synthesised from the schema), against a mocked HTTP layer, on both ``Didit`` +and ``AsyncDidit``, and asserts the method, path, query, credentials, body +encoding and the parsed response type. +""" + +from __future__ import annotations + +import json +import re +import urllib.parse +from collections import defaultdict +from typing import Any + +from .ops import Arg, Method +from .render import HEADER +from .spec import Spec +from .typegen import Registry, _split_union + +Json = Any + +ORG_ID = "0b5e8d2c-0000-4000-8000-0000000000aa" +APP_ID = "0b5e8d2c-0000-4000-8000-0000000000bb" +MAX_DEPTH = 6 + + +def _uuid(n: int) -> str: + return f"00000000-0000-4000-8000-{n:012d}" + + +# Values ------------------------------------------------------------------------------ + + +def _string_sample(schema: Json, n: int) -> str: + fmt = schema.get("format") + if fmt == "date-time": + return "2026-01-01T00:00:00Z" + if fmt == "date": + return "2026-01-01" + if fmt == "uuid": + return _uuid(n) + if fmt in ("uri", "url"): + return "https://example.com/resource" + if fmt == "email": + return "user@example.com" + min_len = schema.get("minLength") or 0 + text = "sample" + if isinstance(min_len, int) and min_len > len(text): + text = text + "x" * (min_len - len(text)) + max_len = schema.get("maxLength") + if isinstance(max_len, int) and max_len < len(text): + text = text[: max(max_len, 1)] + return text + + +def sample(spec: Spec, schema: Json, *, request: bool, depth: int = 0, full: bool = True) -> Json: + """A value that conforms to ``schema``.""" + if not isinstance(schema, dict): + return "value" + if "$ref" in schema: + if depth > MAX_DEPTH: + return None + return sample(spec, spec.resolve(schema), request=request, depth=depth + 1, full=full) + if "allOf" in schema: + merged: dict[str, Json] = {"type": "object", "properties": {}, "required": []} + stack = [*schema["allOf"], {k: v for k, v in schema.items() if k != "allOf"}] + for part in stack: + part = spec.resolve(part) + if "allOf" in part: + stack.extend(part["allOf"]) + merged["properties"].update(part.get("properties", {})) + merged["required"] += part.get("required", []) + if not merged["properties"]: + first = spec.resolve(schema["allOf"][0]) + return sample(spec, first, request=request, depth=depth + 1, full=full) + return sample(spec, merged, request=request, depth=depth + 1, full=full) + for key in ("oneOf", "anyOf"): + if schema.get(key): + return sample(spec, schema[key][0], request=request, depth=depth + 1, full=full) + if "enum" in schema: + values = [v for v in schema["enum"] if v is not None] + return values[0] if values else None + if "example" in schema and conforms(spec, schema, schema["example"], request=request): + example = schema["example"] + if not (request and _has_unknown_keys(spec, schema, example)): + return example + kind = schema.get("type") + if kind == "string": + if schema.get("format") == "binary": + return b"file-content" if request else "binary" + return _string_sample(schema, depth + 1) + if kind == "integer": + minimum = schema.get("minimum") + return int(minimum) if isinstance(minimum, (int, float)) and minimum > 1 else 1 + if kind == "number": + return 1.5 + if kind == "boolean": + return True + if kind == "array": + if depth > MAX_DEPTH: + return [] + item = sample(spec, schema.get("items", {}), request=request, depth=depth + 1, full=full) + min_items = schema.get("minItems") or 1 + return [item] * max(1, min(int(min_items), 3)) + if kind == "object" or "properties" in schema: + props = schema.get("properties", {}) + if not props: + extra = schema.get("additionalProperties") + if isinstance(extra, dict) and extra and depth <= MAX_DEPTH: + return {"key": sample(spec, extra, request=request, depth=depth + 1, full=full)} + return {} + required = set(schema.get("required", [])) + out: dict[str, Json] = {} + for name, prop in props.items(): + resolved = spec.resolve(prop) + if request and resolved.get("readOnly"): + continue + if not request and resolved.get("writeOnly"): + continue + if not full and name not in required: + continue + if depth > MAX_DEPTH and name not in required: + continue + out[name] = sample(spec, prop, request=request, depth=depth + 1, full=full) + return out + return "value" + + +def _has_unknown_keys(spec: Spec, schema: Json, value: Json) -> bool: + schema = spec.resolve(schema) + if isinstance(value, dict) and isinstance(schema, dict) and schema.get("properties"): + props = schema["properties"] + for key, item in value.items(): + if key not in props or _has_unknown_keys(spec, props[key], item): + return True + if isinstance(value, list) and isinstance(schema, dict) and schema.get("type") == "array": + return any(_has_unknown_keys(spec, schema.get("items", {}), item) for item in value) + return False + + +def conforms(spec: Spec, schema: Json, value: Json, *, request: bool, depth: int = 0) -> bool: + """Whether ``value`` matches ``schema`` closely enough to type-check as the SDK's type.""" + if depth > 20 or not isinstance(schema, dict): + return True + schema = spec.resolve(schema) + if value is None: + return schema.get("nullable") is True or not schema + if "allOf" in schema: + return all( + conforms(spec, part, value, request=request, depth=depth + 1) + for part in schema["allOf"] + ) + for key in ("oneOf", "anyOf"): + if key in schema: + return any( + conforms(spec, part, value, request=request, depth=depth + 1) + for part in schema[key] + ) + if "enum" in schema: + if all(isinstance(v, str) for v in schema["enum"] if v is not None): + return isinstance(value, str) + return value in schema["enum"] + kind = schema.get("type") + if kind == "string": + if schema.get("format") == "binary": + return isinstance(value, (bytes, str)) + if not isinstance(value, str): + return False + max_len = schema.get("maxLength") + return not (isinstance(max_len, int) and len(value) > max_len) + if kind == "integer": + return isinstance(value, int) and not isinstance(value, bool) + if kind == "number": + return isinstance(value, (int, float)) and not isinstance(value, bool) + if kind == "boolean": + return isinstance(value, bool) + if kind == "array": + return isinstance(value, list) and all( + conforms(spec, schema.get("items", {}), v, request=request, depth=depth + 1) + for v in value + ) + if kind == "object" or "properties" in schema: + if not isinstance(value, dict): + return False + props = schema.get("properties", {}) + if props: + for name in schema.get("required", []): + if name not in value and not ( + request and spec.resolve(props.get(name, {})).get("readOnly") + ): + return False + for key, item in value.items(): + if key in props: + resolved = spec.resolve(props[key]) + if request and resolved.get("readOnly"): + return False + if not conforms(spec, props[key], item, request=request, depth=depth + 1): + return False + elif request: + return False + return True + extra = schema.get("additionalProperties") + if isinstance(extra, dict) and extra: + return all( + conforms(spec, extra, v, request=request, depth=depth + 1) for v in value.values() + ) + return True + return True + + +_SECRET_KEY = re.compile(r"token|secret|api_?key|password|private_?key|credential", re.IGNORECASE) +_JWT = re.compile(r"^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*$") + + +def scrub(value: Json, key: str | None = None) -> Json: + """Replace credential-like example values with synthetic ones, so that generated + tests never carry anything a secret scanner (or a reader) could take for a secret.""" + if isinstance(value, dict): + return {k: scrub(v, k) for k, v in value.items()} + if isinstance(value, list): + return [scrub(v, key) for v in value] + if isinstance(value, str): + if _JWT.match(value): + return "sample-token" + if key is not None and _SECRET_KEY.search(key) and value: + return "sample-" + ("token" if "token" in key.lower() else "secret") + return value + + +def literal(value: Json) -> str: + """A Python literal for a JSON value.""" + if isinstance(value, bytes): + return repr(value) + if isinstance(value, str): + return json.dumps(value, ensure_ascii=False) + if isinstance(value, bool): + return "True" if value else "False" + if value is None: + return "None" + if isinstance(value, (int, float)): + return repr(value) + if isinstance(value, list): + return "[" + ", ".join(literal(v) for v in value) + "]" + if isinstance(value, dict): + return ( + "{" + + ", ".join( + f"{json.dumps(k, ensure_ascii=False)}: {literal(v)}" for k, v in value.items() + ) + + "}" + ) + raise TypeError(f"Unsupported example value {value!r}") + + +def _query_text(value: Json) -> list[str]: + if isinstance(value, bool): + return ["true" if value else "false"] + if isinstance(value, list): + return [t for v in value for t in _query_text(v)] + return [str(value)] + + +# Tests ----------------------------------------------------------------------------- + + +def _check_expr(ann: str, registry: Registry, imports: set[tuple[str, str]], depth: int = 0) -> str: + parts = _split_union(ann) + checks: list[str] = [] + for part in parts: + checks.append(_part_check(part, registry, imports, depth)) + if "True" in checks: + return "True" + return " or ".join(f"({c})" for c in checks) if len(checks) > 1 else checks[0] + + +def _part_check(part: str, registry: Registry, imports: set[tuple[str, str]], depth: int) -> str: + var = "r" if depth == 0 else f"x{depth}" + if part == "None": + return f"{var} is None" + if part in ("str", "int", "float", "bool", "bytes"): + return f"isinstance({var}, {part})" + if part.startswith("Literal["): + return f"isinstance({var}, str)" + if part in ("object",) or part.startswith("_dt."): + return "True" + if part.startswith("list["): + inner = part[len("list[") : -1] + inner_check = _check_expr(inner, registry, imports, depth + 1) + nxt = f"x{depth + 1}" + if inner_check == "True": + return f"isinstance({var}, list)" + return f"isinstance({var}, list) and all({inner_check} for {nxt} in {var})" + if part.startswith("dict["): + return f"isinstance({var}, dict)" + td = registry.defs.get(part) + if td is None: + return "True" + if td.kind == "alias": + assert td.alias is not None + return _check_expr(td.alias.expr, registry, imports, depth) + imports.add((td.module, td.name)) + return f"isinstance({var}, {td.name})" + + +def _kwargs_for(method: Method) -> list[tuple[Arg, Json]]: + spec = method.op.spec + out: list[tuple[Arg, Json]] = [] + body_args = [a for a in method.kwargs if a.location == "body"] + by_wire = {a.wire: a for a in body_args} + example = method.body_example if method.has_body_example else None + use_example = ( + isinstance(example, dict) + and example + and all( + k in by_wire + and conforms(spec, by_wire[k].schema, v, request=True) + and not _has_unknown_keys(spec, by_wire[k].schema, v) + for k, v in example.items() + ) + and all(a.wire in example for a in body_args if a.required) + ) + chosen: dict[str, Json] = {} + if use_example: + assert isinstance(example, dict) + for key, value in example.items(): + if key in method.file_fields: + value = b"file-content" + chosen[key] = value + else: + for i, arg in enumerate(body_args): + if arg.required or (not any(a.required for a in body_args) and i == 0): + chosen[arg.wire] = sample(spec, arg.schema, request=True, full=False) + for name in method.file_fields: + if name in chosen: + chosen[name] = b"file-content" + for arg in body_args: + if arg.wire in chosen: + out.append((arg, scrub(chosen[arg.wire], arg.wire))) + for arg in (a for a in method.kwargs if a.location == "query"): + if ( + arg.has_example + and conforms(spec, arg.schema, arg.example, request=True) + and arg.example is not None + ): + value = arg.example + else: + value = sample(spec, arg.schema, request=True, full=False) + if value is None: + continue + out.append((arg, scrub(value, arg.wire))) + return out + + +def _path_values(method: Method) -> list[tuple[Arg, str]]: + out: list[tuple[Arg, str]] = [] + for i, arg in enumerate(method.path_args): + value = ( + arg.example + if isinstance(arg.example, str) and re.fullmatch(r"[A-Za-z0-9_-]+", arg.example or "") + else _uuid(i + 1) + ) + out.append((arg, value)) + return out + + +def _response_value(method: Method) -> tuple[int, Json, bool]: + """(status, body, strict): ``strict`` when the body was synthesised from the schema.""" + resp = method.response + spec = method.op.spec + if resp.kind in ("model", "page"): + if resp.has_example and resp.example is not None: + return resp.status, scrub(resp.example), False + return resp.status, scrub(sample(spec, resp.schema, request=False)), True + return resp.status, None, False + + +def _render_test(method: Method, registry: Registry, imports: set[tuple[str, str]]) -> str: + op = method.op + path_values = _path_values(method) + kwargs = _kwargs_for(method) + path = op.path + if op.sdk.scope == "application": + path = path.replace("{organization_id}", ORG_ID).replace("{application_id}", APP_ID) + for arg, value in path_values: + path = path.replace("{" + arg.wire + "}", urllib.parse.quote(value, safe="")) + query: dict[str, list[str]] = defaultdict(list) + json_body: dict[str, Json] = {} + for arg, value in kwargs: + if arg.location == "query": + query[arg.wire].extend(_query_text(value)) + else: + json_body[arg.wire] = value + call_args = [literal(v) for _, v in path_values] + call_args += [f"{a.py}={literal(v)}" for a, v in kwargs] + resource = op.sdk.resource + call = f"c.{resource}.{op.sdk.method}({', '.join(call_args)})" + + status, response, strict = _response_value(method) + multipart = method.body_mode == "multipart" or ( + method.body_mode == "auto" and any(name in json_body for name in method.file_fields) + ) + resp = method.response + if resp.kind == "page": + assert resp.item is not None + item_check = _check_expr(resp.item.expr, registry, imports, depth=1) + items = "x1" + sync_check = f"isinstance(r, SyncPage) and all({item_check} for {items} in r.results)" + async_check = f"isinstance(r, AsyncPage) and all({item_check} for {items} in r.results)" + if item_check == "True": + sync_check, async_check = "isinstance(r, SyncPage)", "isinstance(r, AsyncPage)" + imports.add(("@didit", "AsyncPage")) + imports.add(("@didit", "SyncPage")) + strict_type = None + elif resp.kind == "bytes": + sync_check = async_check = "r == BINARY_BODY" + strict_type = None + elif resp.kind == "none": + sync_check = async_check = "r is None" + strict_type = None + else: + sync_check = async_check = _check_expr(resp.annotation.expr, registry, imports) + strict_type = resp.cast_to if strict and not resp.cast_to.startswith("{") else None + if strict_type: + for dep in resp.annotation.deps: + if not dep.startswith("@"): + imports.add((registry.defs[dep].module, dep)) + + fields: list[str] = [ + f'method="{op.http_method.upper()}"', + f"path={json.dumps(path)}", + f'api="{op.api}"', + f'auth="{op.sdk.auth}"', + ] + if query: + fields.append(f"query={literal(dict(query))}") + if method.body_mode != "none": + if multipart: + text_fields = sorted(k for k in json_body if k not in method.file_fields) + file_fields = sorted(k for k in json_body if k in method.file_fields) + fields.append(f"multipart_fields={literal(text_fields)}") + fields.append(f"multipart_files={literal(file_fields)}") + else: + fields.append(f"json_body={literal(json_body)}") + fields.append(f"status={status}") + if resp.kind == "bytes": + fields.append("response_bytes=BINARY_BODY") + fields.append(f"response_content_type={json.dumps(resp.content_type)}") + elif response is not None: + fields.append(f"response_json={literal(response)}") + if strict_type: + fields.append(f"strict_type={strict_type}") + + name = f"test_{resource.replace('.', '_')}_{op.sdk.method}" + deprecated = op.deprecated + out = [] + if deprecated: + out.append('@pytest.mark.filterwarnings("ignore::DeprecationWarning")\n') + out.append(f"def {name}() -> None:\n") + out.append(f' """``{op.http_method.upper()} {op.path}``"""\n') + out.append(" check_operation(\n") + out.append(" Expect(\n") + out.extend(f" {f},\n" for f in fields) + out.append(" ),\n") + out.append(f" lambda c: {call},\n") + out.append(f" lambda c: {call},\n") + out.append(f" lambda r: {sync_check},\n") + out.append(f" lambda r: {async_check},\n") + out.append(" )\n") + return "".join(out) + + +def render_tests(methods: list[Method], registry: Registry) -> dict[str, str]: + by_root: dict[str, list[Method]] = defaultdict(list) + for method in methods: + by_root[method.resource.split(".")[0]].append(method) + files: dict[str, str] = {"__init__.py": HEADER} + for root, group in sorted(by_root.items()): + imports: set[tuple[str, str]] = set() + bodies = [ + _render_test(m, registry, imports) + for m in sorted(group, key=lambda m: (m.resource, m.name)) + ] + out = [HEADER, f'"""Generated tests of the ``{root}`` resources: one per operation."""\n\n'] + out.append("from __future__ import annotations\n\n") + if any(m.op.deprecated for m in group): + out.append("import pytest\n\n") + didit_names = sorted(n for m, n in imports if m == "@didit") + if didit_names: + out.append(f"from didit import {', '.join(didit_names)}\n") + by_module: dict[str, list[str]] = defaultdict(list) + for module, name in imports: + if module != "@didit": + by_module[module].append(name) + for module in sorted(by_module): + out.append(f"from didit.types.{module} import {', '.join(sorted(by_module[module]))}\n") + harness = ["Expect", "check_operation"] + if any(m.response.kind == "bytes" for m in group): + harness.insert(0, "BINARY_BODY") + out.append(f"from tests.harness import {', '.join(harness)}\n") + for body in bodies: + out.append("\n\n") + out.append(body) + files[f"test_{root}.py"] = "".join(out) + return files diff --git a/codegen/didit_codegen/spec.py b/codegen/didit_codegen/spec.py new file mode 100644 index 0000000..0923370 --- /dev/null +++ b/codegen/didit_codegen/spec.py @@ -0,0 +1,292 @@ +"""Load ``openapi/`` and merge the ``x-didit-sdk`` naming layer (docs/DESIGN.md sections 3 and 4).""" + +from __future__ import annotations + +import json +import pathlib +from dataclasses import dataclass, field +from typing import Any + +HTTP_METHODS = ("get", "put", "post", "delete", "patch", "head", "options") +AUTH_VALUES = frozenset({"api_key", "account_token", "none", "device_token", "public"}) +SDK_KEYS = frozenset({"resource", "method", "auth", "server", "scope"}) +APPLICATION_PREFIX = "/v3/organization/{organization_id}/application/{application_id}/" + +Json = Any + + +class CodegenError(Exception): + """The OpenAPI documents or the overlay cannot be turned into the SDK.""" + + +@dataclass(frozen=True) +class SdkNaming: + resource: str + method: str + auth: str + server: bool + scope: str | None + + @staticmethod + def parse(key: str, raw: Json) -> SdkNaming: + if not isinstance(raw, dict): + raise CodegenError(f"{key}: x-didit-sdk must be an object.") + unknown = set(raw) - SDK_KEYS + if unknown: + raise CodegenError(f"{key}: unknown x-didit-sdk keys {sorted(unknown)}.") + for required in ("resource", "method", "auth", "server"): + if required not in raw: + raise CodegenError(f"{key}: x-didit-sdk is missing {required!r}.") + resource, method, auth, server = raw["resource"], raw["method"], raw["auth"], raw["server"] + scope = raw.get("scope") + if not isinstance(resource, str) or not all( + seg.isidentifier() and seg == seg.lower() for seg in resource.split(".") + ): + raise CodegenError( + f"{key}: resource {resource!r} must be a dot path of snake_case names." + ) + if not isinstance(method, str) or not method.isidentifier() or method != method.lower(): + raise CodegenError(f"{key}: method {method!r} must be a snake_case name.") + if auth not in AUTH_VALUES: + raise CodegenError(f"{key}: auth {auth!r} must be one of {sorted(AUTH_VALUES)}.") + if not isinstance(server, bool): + raise CodegenError(f"{key}: server must be true or false.") + if scope not in (None, "application"): + raise CodegenError(f'{key}: scope must be "application" when present.') + return SdkNaming(resource, method, auth, server, scope) + + def as_json(self) -> dict[str, Json]: + out: dict[str, Json] = { + "auth": self.auth, + "method": self.method, + "resource": self.resource, + "server": self.server, + } + if self.scope: + out["scope"] = self.scope + return out + + +@dataclass +class Spec: + """One OpenAPI document: ``verification`` or ``account``.""" + + name: str + doc: Json + + def resolve(self, node: Json) -> Json: + seen: set[str] = set() + while isinstance(node, dict) and "$ref" in node: + ref = node["$ref"] + if ref in seen: + raise CodegenError(f"{self.name}: circular $ref {ref}.") + seen.add(ref) + node = self.pointer(ref) + return node + + def pointer(self, ref: str) -> Json: + if not ref.startswith("#/"): + raise CodegenError(f"{self.name}: only local $refs are supported, got {ref}.") + cur = self.doc + for part in ref[2:].split("/"): + part = part.replace("~1", "/").replace("~0", "~") + try: + cur = cur[part] + except (KeyError, TypeError) as exc: + raise CodegenError(f"{self.name}: unresolvable $ref {ref}.") from exc + return cur + + @property + def components(self) -> dict[str, Json]: + return dict(self.doc.get("components", {}).get("schemas", {})) + + +@dataclass +class Operation: + key: str + api: str + http_method: str + path: str + op: Json + path_item: Json + spec: Spec + sdk: SdkNaming + deprecated: bool = False + deprecation_note: str | None = None + parameters: list[Json] = field(default_factory=lambda: []) + + @property + def operation_id(self) -> str: + return str(self.op.get("operationId") or self.key) + + +@dataclass +class Model: + specs: dict[str, Spec] + operations: list[Operation] + + @property + def server_operations(self) -> list[Operation]: + return [op for op in self.operations if op.sdk.server] + + +def _key(api: str, method: str, path: str) -> str: + prefix = "account " if api == "account" else "" + return f"{prefix}{method.upper()} {path}" + + +def load(openapi_dir: pathlib.Path) -> Model: + specs = { + "verification": Spec( + "verification", json.loads((openapi_dir / "verification.json").read_text("utf-8")) + ), + "account": Spec("account", json.loads((openapi_dir / "account.json").read_text("utf-8"))), + } + overlay = json.loads((openapi_dir / "overlay.json").read_text("utf-8")) + overlay_ops: dict[str, Json] = dict(overlay.get("operations", {})) + deprecated_path = openapi_dir / "deprecated-operations.json" + deprecated: dict[str, Json] = {} + if deprecated_path.exists(): + deprecated = dict(json.loads(deprecated_path.read_text("utf-8")).get("methods", {})) + + operations: list[Operation] = [] + seen_keys: set[str] = set() + missing: list[str] = [] + for api, spec in specs.items(): + for path, item in spec.doc.get("paths", {}).items(): + item = spec.resolve(item) + for method in HTTP_METHODS: + op = item.get(method) + if op is None: + continue + key = _key(api, method, path) + seen_keys.add(key) + raw = op.get("x-didit-sdk") + if raw is None: + raw = overlay_ops.get(key) + if raw is None: + missing.append(f"{key} (operationId {op.get('operationId', '-')})") + continue + params = _merge_parameters( + spec, item.get("parameters", []), op.get("parameters", []) + ) + operations.append( + Operation( + key=key, + api=api, + http_method=method, + path=path, + op=op, + path_item=item, + spec=spec, + sdk=SdkNaming.parse(key, raw), + deprecated=bool(op.get("deprecated")), + parameters=params, + ) + ) + if missing: + lines = "\n ".join(missing) + raise CodegenError( + "These operations have no SDK naming (x-didit-sdk). Add an entry to openapi/overlay.json " + "(or ask for x-didit-sdk upstream) with resource, method, auth and server; see " + f"docs/DESIGN.md section 4:\n {lines}" + ) + stale = sorted(set(overlay_ops) - seen_keys) + if stale: + raise CodegenError( + "openapi/overlay.json names operations that no longer exist upstream. Run " + "codegen/spec_sync.py, which records removed operations in " + "openapi/deprecated-operations.json, or delete the entries:\n " + "\n ".join(stale) + ) + + operations.extend(_deprecated_operations(deprecated, operations)) + _check_unique(operations) + for operation in operations: + if operation.sdk.scope == "application" and not operation.path.startswith( + APPLICATION_PREFIX + ): + raise CodegenError( + f"{operation.key}: scope application needs a path under {APPLICATION_PREFIX}." + ) + return Model(specs=specs, operations=operations) + + +def _deprecated_operations(deprecated: dict[str, Json], live: list[Operation]) -> list[Operation]: + """The methods ``openapi/deprecated-operations.json`` keeps for one more minor + version: operations removed upstream and the old names of renamed ones. + + Each entry holds its own dereferenced copy of the operation, so it does not + depend on what the current documents still contain. + """ + live_names = {(op.sdk.resource, op.sdk.method): op.key for op in live if op.sdk.server} + out: list[Operation] = [] + for name, entry in sorted(deprecated.items()): + where = f"openapi/deprecated-operations.json {name}" + sdk = SdkNaming.parse(where, entry.get("x-didit-sdk")) + if f"{sdk.resource}.{sdk.method}" != name: + raise CodegenError(f"{where}: the entry name must equal its resource.method.") + provider = live_names.get((sdk.resource, sdk.method)) + if provider is not None: + raise CodegenError( + f"{where}: {provider} provides {name} again, so it is no longer deprecated; " + "delete this entry from openapi/deprecated-operations.json." + ) + key = str(entry.get("operation_key", "")) + api = "account" if key.startswith("account ") else "verification" + method, _, path = key.removeprefix("account ").partition(" ") + if method.lower() not in HTTP_METHODS or not path.startswith("/"): + raise CodegenError(f"{where}: operation_key {key!r} is not 'METHOD /path'.") + replacement = entry.get("replacement") + default_note = ( + f"Renamed to {replacement}; this name is removed in version {entry.get('remove_in')}." + if replacement + else f"Removed from the API contract; this method is removed in version {entry.get('remove_in')}." + ) + op = entry["operation"] + out.append( + Operation( + key=key, + api=api, + http_method=method.lower(), + path=path, + op=op, + path_item={}, + spec=Spec(f"{api} (deprecated)", {"openapi": "3.0.0", "paths": {}}), + sdk=sdk, + deprecated=True, + deprecation_note=entry.get("note") or default_note, + parameters=list(op.get("parameters", [])), + ) + ) + return out + + +def _merge_parameters(spec: Spec, shared: list[Json], own: list[Json]) -> list[Json]: + out: dict[tuple[str, str], Json] = {} + for raw in [*shared, *own]: + param = spec.resolve(raw) + out[(param["in"], param["name"])] = param + return list(out.values()) + + +def _check_unique(operations: list[Operation]) -> None: + by_name: dict[tuple[str, str], str] = {} + for operation in operations: + if not operation.sdk.server: + continue + name = (operation.sdk.resource, operation.sdk.method) + if name in by_name: + raise CodegenError( + f"{operation.key} and {by_name[name]} both map to {name[0]}.{name[1]}; " + "give one of them another method name in openapi/overlay.json." + ) + by_name[name] = operation.key + resources = {op.sdk.resource for op in operations if op.sdk.server} + for operation in operations: + if not operation.sdk.server: + continue + child = f"{operation.sdk.resource}.{operation.sdk.method}" + if child in resources or any(r.startswith(child + ".") for r in resources): + raise CodegenError( + f"{operation.key}: method {operation.sdk.method!r} clashes with the nested resource {child!r}." + ) diff --git a/codegen/didit_codegen/typegen.py b/codegen/didit_codegen/typegen.py new file mode 100644 index 0000000..b7902a6 --- /dev/null +++ b/codegen/didit_codegen/typegen.py @@ -0,0 +1,509 @@ +"""OpenAPI schemas to Python types (docs/DESIGN.md section 5.1). + +Responses become pydantic models (``didit._models.BaseModel``: unknown fields +kept); request bodies become ``TypedDict``s. ``$ref`` components keep their +component name; inline objects are named after their parent plus the property. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass, field +from typing import Any, Literal + +from .naming import identifier, pascal, snake +from .spec import CodegenError, Spec + +Json = Any + +# Markers for names that need an import from outside the generated types. +DATETIME = "@datetime" +DATE = "@date" +LITERAL = "@Literal" +FILE_TYPES = "@FileTypes" +SEQUENCE = "@Sequence" +MAPPING = "@Mapping" +FIELD = "@Field" +REQUIRED = "@Required" + + +@dataclass(frozen=True) +class Ann: + """A type annotation and the names it needs.""" + + expr: str + deps: frozenset[str] = frozenset() + + def optional(self) -> Ann: + if self.expr == "object" or self.expr.endswith(" | None"): + return self + return Ann(f"{self.expr} | None", self.deps) + + +def union(anns: list[Ann]) -> Ann: + parts: list[str] = [] + deps: set[str] = set() + nullable = False + for ann in anns: + deps |= ann.deps + for part in _split_union(ann.expr): + if part == "None": + nullable = True + elif part not in parts: + parts.append(part) + if "object" in parts: + return Ann("object", frozenset(deps)) + expr = " | ".join(parts) if parts else "object" + if nullable: + expr += " | None" + return Ann(expr, frozenset(deps)) + + +def _split_union(expr: str) -> list[str]: + out: list[str] = [] + depth = 0 + current = "" + i = 0 + while i < len(expr): + ch = expr[i] + if ch in "[(": + depth += 1 + elif ch in "])": + depth -= 1 + if depth == 0 and expr.startswith(" | ", i): + out.append(current) + current = "" + i += 3 + continue + current += ch + i += 1 + out.append(current) + return [p.strip() for p in out if p.strip()] + + +@dataclass +class FieldDef: + wire: str + name: str + annotation: Ann + required: bool + description: str | None + deprecated: bool = False + + +@dataclass +class TypeDef: + kind: Literal["model", "typeddict", "alias"] + name: str + module: str + description: str | None = None + fields: list[FieldDef] = field(default_factory=lambda: []) + bases: list[str] = field(default_factory=lambda: []) + alias: Ann | None = None + + @property + def deps(self) -> set[str]: + out: set[str] = set(self.bases) + for f in self.fields: + out |= set(f.annotation.deps) + if self.alias is not None: + out |= set(self.alias.deps) + return out + + +class Registry: + """Every generated type, by name and by module.""" + + def __init__(self) -> None: + self.defs: dict[str, TypeDef] = {} + self.modules: dict[str, list[str]] = {} + + def add(self, td: TypeDef) -> None: + existing = self.defs.get(td.name) + if existing is not None: + raise CodegenError( + f"Two different types are both named {td.name} ({existing.module} and {td.module}); " + "rename one of the operations or properties in openapi/overlay.json." + ) + self.defs[td.name] = td + self.modules.setdefault(td.module, []).append(td.name) + + +def _literal(values: list[Json]) -> str: + return "Literal[" + ", ".join(json.dumps(v, ensure_ascii=False) for v in values) + "]" + + +def _description(schema: Json) -> str | None: + text = schema.get("description") if isinstance(schema, dict) else None + if isinstance(text, str) and text.strip(): + return text.strip() + return None + + +@dataclass +class Usage: + response: set[tuple[str, str]] = field(default_factory=lambda: set()) + request: set[tuple[str, str]] = field(default_factory=lambda: set()) + + +def component_refs(spec: Spec, node: Json, out: set[tuple[str, str]]) -> None: + """Every component reachable from ``node`` (transitively).""" + stack = [node] + while stack: + cur = stack.pop() + if isinstance(cur, dict): + ref = cur.get("$ref") + if isinstance(ref, str): + if ref.startswith("#/components/schemas/"): + key = (spec.name, ref.rsplit("/", 1)[1]) + if key not in out: + out.add(key) + stack.append(spec.pointer(ref)) + else: + stack.append(spec.pointer(ref)) + for k, v in cur.items(): + if k in ("example", "examples"): + continue + stack.append(v) + elif isinstance(cur, list): + stack.extend(cur) + + +class TypeBuilder: + def __init__(self, specs: dict[str, Spec], usage: Usage, base_names: frozenset[str]) -> None: + self.specs = specs + self.usage = usage + self.base_names = base_names + self.registry = Registry() + self._models: dict[tuple[str, str], str] = {} + self._params: dict[tuple[str, str], str] = {} + names_by_spec: dict[str, set[str]] = {} + for spec in specs.values(): + names_by_spec[spec.name] = set(spec.components) + self._component_names: dict[tuple[str, str], str] = {} + for spec in specs.values(): + others = set().union(*(n for s, n in names_by_spec.items() if s != spec.name)) + for name in spec.components: + py = name if name.isidentifier() and name[0].isupper() else pascal(name) + if name in others and spec.name == "account": + py = "Account" + py + self._component_names[(spec.name, name)] = py + + # Components ------------------------------------------------------------ + + def component_name(self, spec: Spec, name: str) -> str: + return self._component_names[(spec.name, name)] + + def component_module(self, spec: Spec, name: str) -> str: + return "components." + snake(self.component_name(spec, name)) + + def component_model(self, spec: Spec, name: str) -> Ann: + key = (spec.name, name) + if key not in self._models: + py = self.component_name(spec, name) + self._models[key] = py + schema = spec.components[name] + module = self.component_module(spec, name) + ann = self._response_inner(spec, schema, py, module, define_as=py) + if ann.expr != py: + self.registry.add(TypeDef("alias", py, module, _description(schema), alias=ann)) + py = self._models[key] + ann = Ann(py, frozenset({py})) + return ann.optional() if spec.components[name].get("nullable") is True else ann + + def component_param(self, spec: Spec, name: str) -> Ann: + key = (spec.name, name) + if key not in self._params: + base = self.component_name(spec, name) + suffix = "Param" if key in self.usage.response else "" + py = base + suffix + self._params[key] = py + schema = spec.components[name] + module = self.component_module(spec, name) + ann = self._param_inner(spec, schema, base, module, suffix, define_as=py) + if ann.expr != py: + model_alias = self.registry.defs.get(base) + if ( + suffix + and model_alias is not None + and model_alias.kind == "alias" + and model_alias.alias == ann + ): + self._params[key] = base + else: + self.registry.add(TypeDef("alias", py, module, _description(schema), alias=ann)) + py = self._params[key] + ann = Ann(py, frozenset({py})) + return ann.optional() if spec.components[name].get("nullable") is True else ann + + # Responses --------------------------------------------------------------- + + def response(self, spec: Spec, schema: Json, hint: str, module: str) -> Ann: + ann = self._response_inner(spec, schema, hint, module) + if isinstance(schema, dict) and schema.get("nullable") is True: + ann = ann.optional() + return ann + + def _response_inner( + self, spec: Spec, schema: Json, hint: str, module: str, define_as: str | None = None + ) -> Ann: + if not isinstance(schema, dict): + return Ann("object") + ref = schema.get("$ref") + if isinstance(ref, str): + if ref.startswith("#/components/schemas/"): + return self.component_model(spec, ref.rsplit("/", 1)[1]) + return self._response_inner(spec, spec.pointer(ref), hint, module, define_as) + if "allOf" in schema: + parts = schema["allOf"] + if len(parts) == 1 and not schema.get("properties"): + return self.response(spec, parts[0], hint, module) + return self._model(spec, schema, define_as or hint, module, mode="response") + for key in ("oneOf", "anyOf"): + if key in schema: + members = [ + self.response(spec, member, self._variant_name(spec, member, hint, i), module) + for i, member in enumerate(schema[key]) + ] + return union(members) + if "enum" in schema: + return self._enum(schema) + kind = schema.get("type") + if kind == "string": + fmt = schema.get("format") + if fmt == "date-time": + return Ann("_dt.datetime", frozenset({DATETIME})) + if fmt == "date": + return Ann("_dt.date", frozenset({DATE})) + if fmt == "binary": + return Ann("bytes") + return Ann("str") + if kind == "integer": + return Ann("int") + if kind == "number": + return Ann("float") + if kind == "boolean": + return Ann("bool") + if kind == "array": + item = self.response(spec, schema.get("items", {}), hint, module) + return Ann(f"list[{item.expr}]", item.deps) + if kind == "object" or "properties" in schema: + if schema.get("properties"): + return self._model(spec, schema, define_as or hint, module, mode="response") + extra = schema.get("additionalProperties") + if isinstance(extra, dict) and extra: + value = self.response(spec, extra, hint + "Value", module) + return Ann(f"dict[str, {value.expr}]", value.deps) + return Ann("dict[str, object]") + return Ann("object") + + # Request parameters ----------------------------------------------------- + + def param(self, spec: Spec, schema: Json, hint: str, module: str, suffix: str = "") -> Ann: + ann = self._param_inner(spec, schema, hint, module, suffix) + if isinstance(schema, dict) and schema.get("nullable") is True: + ann = ann.optional() + return ann + + def _param_inner( + self, + spec: Spec, + schema: Json, + hint: str, + module: str, + suffix: str = "", + define_as: str | None = None, + ) -> Ann: + if not isinstance(schema, dict): + return Ann("object") + ref = schema.get("$ref") + if isinstance(ref, str): + if ref.startswith("#/components/schemas/"): + return self.component_param(spec, ref.rsplit("/", 1)[1]) + return self._param_inner(spec, spec.pointer(ref), hint, module, suffix, define_as) + if "allOf" in schema: + parts = schema["allOf"] + if len(parts) == 1 and not schema.get("properties"): + return self.param(spec, parts[0], hint, module, suffix) + return self._model( + spec, + schema, + define_as or hint + suffix, + module, + mode="param", + suffix=suffix, + hint=hint, + ) + for key in ("oneOf", "anyOf"): + if key in schema: + members = [ + self.param( + spec, member, self._variant_name(spec, member, hint, i), module, suffix + ) + for i, member in enumerate(schema[key]) + ] + return union(members) + if "enum" in schema: + return self._enum(schema) + kind = schema.get("type") + if kind == "string": + fmt = schema.get("format") + if fmt == "date-time": + return Ann("str | _dt.datetime", frozenset({DATETIME})) + if fmt == "date": + return Ann("str | _dt.date", frozenset({DATE})) + if fmt == "binary": + return Ann("FileTypes", frozenset({FILE_TYPES})) + return Ann("str") + if kind == "integer": + return Ann("int") + if kind == "number": + return Ann("float") + if kind == "boolean": + return Ann("bool") + if kind == "array": + item = self.param(spec, schema.get("items", {}), hint, module, suffix) + return Ann(f"Sequence[{item.expr}]", item.deps | {SEQUENCE}) + if kind == "object" or "properties" in schema: + if schema.get("properties"): + return self._model( + spec, + schema, + define_as or hint + suffix, + module, + mode="param", + suffix=suffix, + hint=hint, + ) + extra = schema.get("additionalProperties") + if isinstance(extra, dict) and extra: + value = self.param(spec, extra, hint + "Value", module, suffix) + return Ann(f"Mapping[str, {value.expr}]", value.deps | {MAPPING}) + return Ann("Mapping[str, object]", frozenset({MAPPING})) + return Ann("object") + + # Shared ------------------------------------------------------------------ + + def _variant_name(self, spec: Spec, member: Json, hint: str, index: int) -> str: + title = member.get("title") if isinstance(member, dict) else None + if isinstance(title, str) and title.strip(): + return hint + pascal(title) + return f"{hint}Variant{index + 1}" + + def _enum(self, schema: Json) -> Ann: + values = [v for v in schema["enum"] if v is not None] + kind = schema.get("type") + if values and all(isinstance(v, str) for v in values): + return Ann(f"{_literal(values)} | str", frozenset({LITERAL})) + if kind == "integer" or ( + values and all(isinstance(v, int) and not isinstance(v, bool) for v in values) + ): + return Ann("int") + if kind == "boolean": + return Ann("bool") + if kind == "number": + return Ann("float") + return Ann("object") + + def _collect(self, spec: Spec, schema: Json) -> tuple[dict[str, Json], set[str], list[Json]]: + """Merge ``allOf`` parts into one property map; returns (properties, required, ref parts).""" + props: dict[str, Json] = {} + required: set[str] = set(schema.get("required", [])) + refs: list[Json] = [] + for part in schema.get("allOf", []): + if isinstance(part, dict) and "$ref" in part: + refs.append(part) + target = spec.resolve(part) + sub_props, sub_required, _ = self._collect(spec, target) + props.update(sub_props) + required |= sub_required + props.update(schema.get("properties", {})) + return props, required, refs + + def _model( + self, + spec: Spec, + schema: Json, + name: str, + module: str, + *, + mode: Literal["response", "param"], + suffix: str = "", + hint: str | None = None, + ) -> Ann: + hint = hint or name + if name in self.registry.defs: + raise CodegenError(f"Two different types are both named {name} (in {module}).") + props, required, ref_parts = self._collect(spec, schema) + bases: list[str] = [] + inherited: set[str] = set() + own_props = props + if len(ref_parts) == 1: + target = spec.resolve(ref_parts[0]) + ref_name = ref_parts[0]["$ref"] + if ( + ref_name.startswith("#/components/schemas/") + and isinstance(target, dict) + and target.get("properties") + and "allOf" not in target + and target.get("type", "object") == "object" + ): + comp = ref_name.rsplit("/", 1)[1] + base_ann = ( + self.component_model(spec, comp) + if mode == "response" + else self.component_param(spec, comp) + ) + base_td = self.registry.defs.get(base_ann.expr) + if base_td is not None and base_td.kind == ( + "model" if mode == "response" else "typeddict" + ): + bases.append(base_ann.expr) + inherited = set(target.get("properties", {})) + base_required = set(target.get("required", [])) + own_props = { + k: v + for k, v in props.items() + if k not in inherited or (k in required) != (k in base_required) + } + td = TypeDef( + "model" if mode == "response" else "typeddict", + name, + module, + _description(schema), + bases=bases, + ) + # Register first so that nested names can never collide with it. + self.registry.add(td) + taken = set(self.base_names) if mode == "response" else set() + for wire, prop in own_props.items(): + resolved = spec.resolve(prop) if isinstance(prop, dict) else {} + if mode == "response" and ( + prop.get("writeOnly") is True or resolved.get("writeOnly") is True + ): + continue + if mode == "param" and resolved.get("readOnly") is True: + continue + child_hint = hint + pascal(wire) + if mode == "response": + ann = self.response(spec, prop, child_hint, module) + else: + ann = self.param(spec, prop, child_hint, module, suffix) + is_required = wire in required + if mode == "response" and not is_required: + ann = ann.optional() + py_name = identifier(wire, taken=frozenset(taken)) + taken.add(py_name) + td.fields.append( + FieldDef( + wire=wire, + name=py_name, + annotation=ann, + required=is_required, + description=_description(prop) or _description(resolved), + deprecated=bool(resolved.get("deprecated")) + if isinstance(resolved, dict) + else False, + ) + ) + return Ann(name, frozenset({name})) diff --git a/codegen/generate.py b/codegen/generate.py new file mode 100644 index 0000000..85f3571 --- /dev/null +++ b/codegen/generate.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Regenerate the SDK's generated layer from ``openapi/``. + +uv run python codegen/generate.py # write src/didit/{types,resources}, tests/generated +uv run python codegen/generate.py --check # fail when the committed code is stale +""" + +from __future__ import annotations + +import pathlib +import sys + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent)) + +from didit_codegen.cli import main + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/codegen/spec_sync.py b/codegen/spec_sync.py new file mode 100644 index 0000000..08f99c2 --- /dev/null +++ b/codegen/spec_sync.py @@ -0,0 +1,693 @@ +#!/usr/bin/env python3 +"""Sync ``openapi/`` with the published API contract (docs/DESIGN.md section 3.1). + + uv run python codegen/spec_sync.py --summary spec-sync-summary.md + +Downloads https://docs.didit.me/openapi-25.json and openapi-auth.json, and when +either changed: + +- checks that each download is an OpenAPI 3 document with paths, and writes + nothing otherwise; +- writes them byte for byte to ``openapi/verification.json`` / ``account.json`` + and records URL, SHA-256 and fetch time in ``openapi/SOURCE.json``; +- drops ``overlay.json`` entries that upstream now carries as ``x-didit-sdk``; +- never deletes a public method silently: a server method whose operation was + removed, or whose naming changed upstream (a rename), is recorded, fully + dereferenced, in ``openapi/deprecated-operations.json``, so the SDK keeps it + as a deprecated method until the next minor version (``remove_in``). An + operation whose SDK name reappears on another path counts as moved, not removed; +- writes a Markdown summary of added, removed, renamed and changed operations + and schemas. Changes are compared field by field, nested objects, arrays, + types, enums, nullability and required fields included, and every change + that can break code written against the SDK is listed under "Breaking changes". + +It prints ``changed=true|false`` to ``$GITHUB_OUTPUT`` when that is set. The +generator, not this script, regenerates the code. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +import pathlib +import re +import sys +import urllib.request +from typing import Any + +REPO = pathlib.Path(__file__).resolve().parents[1] +UPSTREAM = { + "verification.json": "https://docs.didit.me/openapi-25.json", + "account.json": "https://docs.didit.me/openapi-auth.json", +} +PREFIXES = {"verification.json": "", "account.json": "account "} +METHODS = ("get", "put", "post", "delete", "patch", "head", "options") +# Keys that document a schema without changing what is sent or received. +DOC_KEYS = frozenset({"description", "summary", "title", "example", "examples", "externalDocs"}) +MAX_NOTES = 25 +Json = Any + + +class SpecSyncError(Exception): + """A download is not a document the SDK can be generated from.""" + + +def fetch(url: str) -> bytes: + request = urllib.request.Request(url, headers={"User-Agent": "didit-sdk-python spec-sync"}) # noqa: S310 + with urllib.request.urlopen(request, timeout=60) as response: # noqa: S310 - fixed https URLs + data: bytes = response.read() + return data + + +def check_openapi(name: str, data: bytes) -> Json: + """Parse ``data`` and refuse anything that is not an OpenAPI 3 document with paths + (an error page or a maintenance JSON must never replace the contract).""" + try: + doc = json.loads(data) + except ValueError as exc: + raise SpecSyncError(f"{name}: the download is not JSON ({exc}).") from exc + if not isinstance(doc, dict): + raise SpecSyncError(f"{name}: the download is not a JSON object.") + version = doc.get("openapi") + if not isinstance(version, str) or not version.startswith("3."): + raise SpecSyncError( + f"{name}: the download is not an OpenAPI 3 document (openapi={version!r})." + ) + paths = doc.get("paths") + if not isinstance(paths, dict) or not paths: + raise SpecSyncError(f"{name}: the download has no paths.") + return doc + + +def _pointer(doc: Json, ref: str) -> Json: + cur = doc + for part in ref[2:].split("/"): + cur = cur[part.replace("~1", "/").replace("~0", "~")] + return cur + + +def dereference(doc: Json, node: Json, depth: int = 0) -> Json: + """``node`` with every local ``$ref`` inlined (self-contained for deprecated operations).""" + if depth > 64: + return {} + if isinstance(node, dict): + if "$ref" in node and isinstance(node["$ref"], str): + try: + found = _pointer(doc, node["$ref"]) + except (KeyError, IndexError, TypeError): + # A dangling reference upstream: keep it visible; the generator reports it. + return {"x-unresolved-ref": node["$ref"]} + target = dereference(doc, found, depth + 1) + extra = {k: v for k, v in node.items() if k != "$ref"} + return {**target, **extra} if isinstance(target, dict) else target + return {k: dereference(doc, v, depth + 1) for k, v in node.items()} + if isinstance(node, list): + return [dereference(doc, v, depth + 1) for v in node] + return node + + +def operations(doc: Json, prefix: str) -> dict[str, Json]: + out: dict[str, Json] = {} + for path, item in doc.get("paths", {}).items(): + for method in METHODS: + if method in item: + op = dict(item[method]) + if item.get("parameters"): + op["parameters"] = [*item["parameters"], *op.get("parameters", [])] + out[f"{prefix}{method.upper()} {path}"] = op + return out + + +# Schema comparison ---------------------------------------------------------------------- + + +def _shape(node: Json) -> Json: + """``node`` without documentation, so that only what is sent or received compares.""" + if isinstance(node, dict): + return { + k: _shape(v) + for k, v in node.items() + if k not in DOC_KEYS and not str(k).startswith("x-") + } + if isinstance(node, list): + return [_shape(v) for v in node] + return node + + +def _types(schema: Json) -> set[str]: + raw = schema.get("type") + types = set(raw) if isinstance(raw, list) else ({raw} if isinstance(raw, str) else set()) + if schema.get("nullable") is True: + types.add("null") + for variant in schema.get("anyOf", []) + schema.get("oneOf", []): + if isinstance(variant, dict) and variant.get("type") == "null": + types.add("null") + return types + + +def _object_props(schema: Json) -> tuple[dict[str, Json], set[str]]: + props: dict[str, Json] = {} + required: set[str] = set() + stack = [schema] + while stack: + cur = stack.pop() + if not isinstance(cur, dict): + continue + props.update(cur.get("properties") or {}) + required |= set(cur.get("required") or []) + stack.extend(cur.get("allOf") or []) + return props, required + + +def _variants(schema: Json, keyword: str) -> list[Json]: + return [ + v + for v in schema.get(keyword) or [] + if not (isinstance(v, dict) and v.get("type") == "null") + ] + + +def diff_schema( + old: Json, + new: Json, + where: str, + *, + request: bool, + notes: list[tuple[str, bool]], + depth: int = 0, +) -> None: + """Append ``(note, breaking)`` for every difference between two dereferenced schemas. + + ``request`` tells which side the schema is on: removing an enum value breaks + callers that send it, while a response enum is open (DESIGN 5.1) and gaining + or losing a value breaks nobody; a field that becomes required breaks callers, + a response field that is no longer always present breaks readers. + """ + if depth > 16 or _shape(old) == _shape(new): + return + if not isinstance(old, dict) or not isinstance(new, dict): + notes.append((f"{where}: schema replaced", True)) + return + old_types, new_types = _types(old), _types(new) + old_plain, new_plain = old_types - {"null"}, new_types - {"null"} + if old_plain and new_plain and old_plain != new_plain: + widened = old_plain == {"integer"} and new_plain == {"number"} + narrowed = old_plain == {"number"} and new_plain == {"integer"} + harmless = (request and widened) or (not request and narrowed) + notes.append( + ( + f"{where}: type {'/'.join(sorted(old_plain))} -> {'/'.join(sorted(new_plain))}", + not harmless, + ) + ) + return + if "null" in new_types and "null" not in old_types and not request: + notes.append((f"{where}: can now be null", True)) + if "null" in old_types and "null" not in new_types and request: + notes.append((f"{where}: no longer accepts null", True)) + + old_enum, new_enum = old.get("enum"), new.get("enum") + if isinstance(old_enum, list) and isinstance(new_enum, list): + gone = [v for v in old_enum if v not in new_enum] + added = [v for v in new_enum if v not in old_enum] + if gone: + notes.append((f"{where}: values removed {json.dumps(gone)}", request)) + if added: + notes.append((f"{where}: values added {json.dumps(added)}", False)) + elif isinstance(new_enum, list) and not isinstance(old_enum, list): + notes.append((f"{where}: now limited to {json.dumps(new_enum)}", request)) + elif isinstance(old_enum, list): + notes.append((f"{where}: no longer limited to fixed values", False)) + + old_props, old_req = _object_props(old) + new_props, new_req = _object_props(new) + for name in sorted(set(old_props) - set(new_props)): + notes.append((f"{where}.{name}: removed", True)) + for name in sorted(set(new_props) - set(old_props)): + required = request and name in new_req + notes.append((f"{where}.{name}: added{' (required)' if required else ''}", required)) + for name in sorted(set(old_props) & set(new_props)): + if request and name in new_req and name not in old_req: + notes.append((f"{where}.{name}: now required", True)) + if not request and name in old_req and name not in new_req: + notes.append((f"{where}.{name}: no longer always present", True)) + diff_schema( + old_props[name], new_props[name], f"{where}.{name}", + request=request, notes=notes, depth=depth + 1, + ) # fmt: skip + for key, suffix in (("items", "[]"), ("additionalProperties", "{}")): + if isinstance(old.get(key), dict) or isinstance(new.get(key), dict): + diff_schema( + old.get(key) or {}, new.get(key) or {}, f"{where}{suffix}", + request=request, notes=notes, depth=depth + 1, + ) # fmt: skip + for keyword in ("oneOf", "anyOf"): + old_vars, new_vars = _variants(old, keyword), _variants(new, keyword) + if len(new_vars) < len(old_vars): + notes.append( + (f"{where}: {len(old_vars) - len(new_vars)} {keyword} variant(s) removed", True) + ) + elif len(new_vars) > len(old_vars): + notes.append( + (f"{where}: {len(new_vars) - len(old_vars)} {keyword} variant(s) added", False) + ) + else: + for index, (a, b) in enumerate(zip(old_vars, new_vars, strict=True)): + diff_schema( + a, b, f"{where}<{keyword} {index + 1}>", + request=request, notes=notes, depth=depth + 1, + ) # fmt: skip + + +def _body(op: Json) -> tuple[set[str], Json]: + content = (op.get("requestBody") or {}).get("content") or {} + media = content.get("application/json") or content.get("multipart/form-data") or {} + return set(content), media.get("schema", {}) + + +def _success(op: Json) -> tuple[set[str], Json]: + for code, response in sorted((op.get("responses") or {}).items()): + if str(code).startswith("2"): + content = response.get("content") or {} + return set(content), (content.get("application/json") or {}).get("schema", {}) + return set(), {} + + +def describe_change(old_doc: Json, old: Json, new_doc: Json, new: Json) -> tuple[list[str], bool]: + """Human-readable differences of one operation, and whether any is breaking.""" + found: list[tuple[str, bool]] = [] + old_params = { + (p.get("in"), p.get("name")): p for p in dereference(old_doc, old.get("parameters", [])) + } + new_params = { + (p.get("in"), p.get("name")): p for p in dereference(new_doc, new.get("parameters", [])) + } + for key in sorted(set(old_params) - set(new_params), key=str): + found.append((f"parameter removed: {key[1]} ({key[0]})", True)) + for key in sorted(set(new_params) - set(old_params), key=str): + required = bool(new_params[key].get("required")) + found.append( + (f"parameter added: {key[1]} ({key[0]}{', required' if required else ''})", required) + ) + for key in sorted(set(old_params) & set(new_params), key=str): + if not old_params[key].get("required") and new_params[key].get("required"): + found.append((f"parameter now required: {key[1]}", True)) + diff_schema( + old_params[key].get("schema") or {}, new_params[key].get("schema") or {}, + f"parameter {key[1]}", request=True, notes=found, + ) # fmt: skip + + old_types, old_body = _body(old) + new_types, new_body = _body(new) + if old_types != new_types and old_types and new_types: + found.append( + ( + f"request content type {sorted(old_types)} -> {sorted(new_types)}", + not old_types <= new_types, + ) + ) + if (old.get("requestBody") or {}).get("required") is not True and ( + new.get("requestBody") or {} + ).get("required") is True: + found.append(("request body now required", True)) + diff_schema( + dereference(old_doc, old_body), dereference(new_doc, new_body), "body", + request=True, notes=found, + ) # fmt: skip + + old_types, old_resp = _success(old) + new_types, new_resp = _success(new) + if old_types != new_types: + found.append((f"response content type {sorted(old_types)} -> {sorted(new_types)}", True)) + diff_schema( + dereference(old_doc, old_resp), dereference(new_doc, new_resp), "response", + request=False, notes=found, + ) # fmt: skip + + if old.get("deprecated") is not True and new.get("deprecated") is True: + found.append(("marked deprecated upstream", False)) + if old.get("x-didit-sdk") != new.get("x-didit-sdk") and new.get("x-didit-sdk") is not None: + found.append((f"x-didit-sdk now {json.dumps(new['x-didit-sdk'], sort_keys=True)}", False)) + if not found: + return ["descriptions, examples or other details changed"], False + breaking = any(b for _, b in found) + # Breaking notes first, then the rest, capped so that one huge change stays readable. + ordered = [n for n, b in found if b] + [n for n, b in found if not b] + notes = [f"{n} (breaking)" if (n, True) in found else n for n in ordered] + if len(notes) > MAX_NOTES: + notes = [*notes[:MAX_NOTES], f"and {len(notes) - MAX_NOTES} more differences"] + return notes, breaking + + +# Versions ------------------------------------------------------------------------------- + + +def _version() -> str: + text = (REPO / "src" / "didit" / "_version.py").read_text("utf-8") + match = re.search(r'__version__ = "([^"]+)"', text) + return match.group(1) if match else "0.0.0" + + +def next_minor(version: str) -> str: + """The version in which a method deprecated in ``version`` is deleted.""" + match = re.match(r"(\d+)\.(\d+)", version) + if not match: + return "0.1.0" + return f"{match.group(1)}.{int(match.group(2)) + 1}.0" + + +# The sync ------------------------------------------------------------------------------- + + +def _name(naming: Json) -> str: + return f"{naming['resource']}.{naming['method']}" + + +def sync( + openapi_dir: pathlib.Path, + downloads: dict[str, bytes], + *, + today: dt.date, + now: dt.datetime, + version: str | None = None, +) -> tuple[bool, str]: + """Apply ``downloads`` (file name -> bytes) to ``openapi_dir``; returns (changed, summary).""" + old_docs: dict[str, Json] = {} + new_docs: dict[str, Json] = {} + changed_files: list[str] = [] + for name, data in downloads.items(): + new_docs[name] = check_openapi(name, data) + path = openapi_dir / name + old_bytes = path.read_bytes() if path.exists() else b"{}" + old_docs[name] = json.loads(old_bytes) + if old_bytes != data: + changed_files.append(name) + if not changed_files: + return False, "No upstream change.\n" + + version = version or _version() + remove_in = next_minor(version) + overlay_path = openapi_dir / "overlay.json" + overlay = json.loads(overlay_path.read_text("utf-8")) + old_overlay = dict(overlay["operations"]) + deprecated_path = openapi_dir / "deprecated-operations.json" + deprecated: dict[str, Any] = ( + json.loads(deprecated_path.read_text("utf-8")) + if deprecated_path.exists() + else {"description": "", "methods": {}} + ) + deprecated.setdefault("methods", {}) + old_ops: dict[str, tuple[str, Json]] = {} + new_ops: dict[str, tuple[str, Json]] = {} + for name in downloads: + for key, op in operations(old_docs[name], PREFIXES[name]).items(): + old_ops[key] = (name, op) + for key, op in operations(new_docs[name], PREFIXES[name]).items(): + new_ops[key] = (name, op) + + def old_naming(key: str) -> Json: + return old_ops[key][1].get("x-didit-sdk") or old_overlay.get(key) + + def new_naming(key: str) -> Json: + return new_ops[key][1].get("x-didit-sdk") or old_overlay.get(key) + + added = sorted(set(new_ops) - set(old_ops)) + removed = sorted(set(old_ops) - set(new_ops)) + common = sorted(set(old_ops) & set(new_ops)) + # Server method name -> the operation that provides it after the sync. + new_server_names = { + _name(n): key for key in new_ops if (n := new_naming(key)) and n.get("server") + } + old_server_names = { + _name(n): key for key in old_ops if (n := old_naming(key)) and n.get("server") + } + + lines: list[str] = [ + f"Spec sync {today.isoformat()}. Upstream files changed: {', '.join(changed_files)}.", + "", + ] + breaking: list[str] = [] + kept: list[str] = [] + + def keep(naming: Json, key: str, reason: str, replacement: str | None) -> None: + """Record a method that would disappear as deprecated until ``remove_in``.""" + doc_name, op = old_ops[key] + method = _name(naming) + if replacement: + note = ( + f"Renamed to {replacement} on {today.isoformat()}; " + f"this name is removed in version {remove_in}." + ) + else: + note = ( + f"Removed from the API contract on {today.isoformat()}; " + f"this method is removed in version {remove_in}." + ) + entry: dict[str, Any] = { + "reason": reason, + "operation_key": key, + "x-didit-sdk": naming, + "operation": dereference(old_docs[doc_name], op), + "since": today.isoformat(), + "deprecated_since": version, + "remove_in": remove_in, + "note": note, + } + if replacement: + entry["replacement"] = replacement + deprecated["methods"][method] = entry + kept.append(method) + + lines.append(f"## Added operations ({len(added)})") + for key in added: + n = new_naming(key) + if n is None: + lines.append( + f"- `{key}`: **no SDK naming**; add an entry to `openapi/overlay.json` (DESIGN section 4)" + ) + elif n.get("server"): + lines.append(f"- `{key}` -> `{_name(n)}`") + else: + lines.append(f"- `{key}` (not exposed by server SDKs: {n['auth']})") + lines.append("") + + lines.append(f"## Removed operations ({len(removed)})") + for key in removed: + n = old_naming(key) + if n is None: + lines.append(f"- `{key}`") + elif not n.get("server"): + lines.append(f"- `{key}` (not exposed by server SDKs)") + elif _name(n) in new_server_names: + lines.append( + f"- `{key}` moved to `{new_server_names[_name(n)]}`; `{_name(n)}` keeps its name" + ) + else: + breaking.append( + f"`{_name(n)}` was removed upstream ({key}); kept as a deprecated method until " + f"version {remove_in}" + ) + lines.append( + f"- `{key}` (`{_name(n)}`): kept as a deprecated method until version {remove_in}, " + "recorded in `openapi/deprecated-operations.json`" + ) + keep(n, key, "removed", None) + overlay["operations"].pop(key, None) + lines.append("") + + renames: list[str] = [] + for key in common: + before, after = old_naming(key), new_naming(key) + if not before or not before.get("server"): + continue + if after and after.get("server") and _name(after) == _name(before): + continue + if _name(before) in new_server_names: + continue # another operation now provides the name (swapped or moved) + if after is None: + # The generator fails until the operation is named again, so this cannot + # ship silently; the overlay entry decides whether the name survives. + renames.append( + f"- `{key}` lost its SDK naming: add `{_name(before)}` back to " + "`openapi/overlay.json` (the generator fails until it is named)" + ) + breaking.append(f"`{_name(before)}` ({key}) has no SDK naming any more") + elif not after.get("server"): + # Server SDKs never expose device or end-user token operations (DESIGN 4), + # and an API key could not call it any more, so the method goes now. + renames.append( + f"- `{key}` (`{_name(before)}`) is no longer a server operation " + f"(auth {after.get('auth')}); the method is removed" + ) + breaking.append( + f"`{_name(before)}` ({key}) is removed: the operation now needs " + f"{after.get('auth')} and server SDKs do not expose it" + ) + else: + renames.append( + f"- `{key}`: `{_name(before)}` renamed to `{_name(after)}`; the old name stays " + f"as a deprecated alias until version {remove_in}" + ) + breaking.append( + f"`{_name(before)}` was renamed upstream to `{_name(after)}` ({key}); the old " + f"name is a deprecated alias until version {remove_in}" + ) + keep(before, key, "renamed", _name(after)) + lines.append(f"## Renamed methods ({len(renames)})") + lines.extend(renames) + lines.append("") + + # A name that stays but now belongs to another operation: moved to a new path, + # or reassigned (swapped, or reused by another operation). Compare what the + # method sends and receives before and after. + moves: list[str] = [] + for method in sorted(set(old_server_names) & set(new_server_names)): + old_key, new_key = old_server_names[method], new_server_names[method] + if old_key == new_key: + continue + old_name, old = old_ops[old_key] + new_name, new = new_ops[new_key] + notes, is_breaking = describe_change(old_docs[old_name], old, new_docs[new_name], new) + if notes == ["descriptions, examples or other details changed"]: + notes = ["same parameters, body and response"] + detail = "; ".join(notes) + if old_key in new_ops: + moves.append( + f"- `{method}` **(breaking)** now calls `{new_key}` instead of `{old_key}` " + f"(which is still in the API): {detail}" + ) + breaking.append( + f"`{method}` now calls `{new_key}` instead of `{old_key}`, a different operation" + ) + else: + marker = " **(breaking)**" if is_breaking else "" + moves.append(f"- `{method}`{marker} moved from `{old_key}` to `{new_key}`: {detail}") + if is_breaking: + breaking.append( + f"`{method}` moved from `{old_key}` to `{new_key}`: " + + "; ".join(n for n in notes if n.endswith("(breaking)")) + ) + lines.append(f"## Moved or reassigned methods ({len(moves)})") + lines.extend(moves) + lines.append("") + + for method in sorted(deprecated["methods"]): + if method in new_server_names and method not in kept: + del deprecated["methods"][method] + lines.append( + f"- `{method}` is provided again by `{new_server_names[method]}`; " + "its deprecation entry was dropped." + ) + + changes: list[str] = [] + for key in common: + old_name, old = old_ops[key] + new_name, new = new_ops[key] + if old == new and old_docs[old_name].get("components") == new_docs[new_name].get( + "components" + ): + continue + notes, is_breaking = describe_change(old_docs[old_name], old, new_docs[new_name], new) + if old == new and notes == ["descriptions, examples or other details changed"]: + continue + marker = " **(breaking)**" if is_breaking else "" + changes.append(f"- `{key}`{marker}: " + "; ".join(notes)) + if is_breaking: + breaking.append(f"`{key}`: " + "; ".join(n for n in notes if n.endswith("(breaking)"))) + lines.append(f"## Changed operations ({len(changes)})") + lines.extend(changes) + lines.append("") + + lines.append("## Schemas") + for name in downloads: + old_schemas = old_docs[name].get("components", {}).get("schemas", {}) + new_schemas = new_docs[name].get("components", {}).get("schemas", {}) + gone = sorted(set(old_schemas) - set(new_schemas)) + for schema in gone: + breaking.append(f"{name}: schema `{schema}` removed (its type leaves `didit.types`)") + for label, names in ( + ("added", sorted(set(new_schemas) - set(old_schemas))), + ("removed", gone), + ( + "changed", + sorted( + k + for k in set(old_schemas) & set(new_schemas) + if old_schemas[k] != new_schemas[k] + ), + ), + ): + if names: + lines.append(f"- {name} {label}: " + ", ".join(f"`{n}`" for n in names)) + lines.append("") + + dropped: list[str] = [] + for key in list(overlay["operations"]): + entry = new_ops.get(key) + if entry is not None and entry[1].get("x-didit-sdk") is not None: + if entry[1]["x-didit-sdk"] != overlay["operations"][key]: + lines.append( + f"- `{key}`: upstream x-didit-sdk differs from the overlay; upstream wins." + ) + dropped.append(key) + del overlay["operations"][key] + if dropped: + lines.append(f"Dropped {len(dropped)} overlay entries that upstream now carries.") + lines.append("") + + lines.insert(2, "## Breaking changes") + lines.insert(3, "\n".join(f"- {b}" for b in breaking) if breaking else "None detected.") + lines.insert(4, "") + + for name, data in downloads.items(): + (openapi_dir / name).write_bytes(data) + source_path = openapi_dir / "SOURCE.json" + source = json.loads(source_path.read_text("utf-8")) if source_path.exists() else {} + for name, data in downloads.items(): + source[name] = { + "url": UPSTREAM.get(name, source.get(name, {}).get("url", "")), + "sha256": hashlib.sha256(data).hexdigest(), + "fetched_at": now.strftime("%Y-%m-%dT%H:%M:%SZ"), + } + # Same layout as the seeded files (one-space indent, no trailing newline), so diffs stay small. + source_path.write_text(json.dumps(source, indent=1, ensure_ascii=False), "utf-8") + overlay_path.write_text(json.dumps(overlay, indent=1, ensure_ascii=False), "utf-8") + deprecated_path.write_text(json.dumps(deprecated, indent=1, ensure_ascii=False) + "\n", "utf-8") + return True, "\n".join(lines).rstrip() + "\n" + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument("--summary", type=pathlib.Path, help="write the Markdown summary here") + parser.add_argument( + "--from-dir", type=pathlib.Path, help="read the upstream files from a directory instead" + ) + args = parser.parse_args(argv) + if args.from_dir: + downloads = {name: (args.from_dir / name).read_bytes() for name in UPSTREAM} + else: + downloads = {name: fetch(url) for name, url in UPSTREAM.items()} + now = dt.datetime.now(dt.timezone.utc) + try: + changed, summary = sync(REPO / "openapi", downloads, today=now.date(), now=now) + except SpecSyncError as exc: + print(f"spec sync refused the download, nothing was written: {exc}", file=sys.stderr) + return 1 + if args.summary: + args.summary.write_text(summary, "utf-8") + print(summary) + output = os.environ.get("GITHUB_OUTPUT") + if output: + with open(output, "a", encoding="utf-8") as fh: + fh.write(f"changed={'true' if changed else 'false'}\n") + fh.write(f"date={now.date().isoformat()}\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/codegen/tests/test_codegen.py b/codegen/tests/test_codegen.py new file mode 100644 index 0000000..f3b6bee --- /dev/null +++ b/codegen/tests/test_codegen.py @@ -0,0 +1,623 @@ +"""Tests of the generator and the spec sync (they never touch the network).""" + +from __future__ import annotations + +import datetime as dt +import json +import pathlib +import shutil +import sys +from typing import Any + +import pytest + +CODEGEN = pathlib.Path(__file__).resolve().parents[1] +REPO = CODEGEN.parent +sys.path.insert(0, str(CODEGEN)) + +import spec_sync # noqa: E402 +from didit_codegen import naming # noqa: E402 +from didit_codegen.generate import GENERATED_ROOTS, build, generate # noqa: E402 +from didit_codegen.render import HEADER # noqa: E402 +from didit_codegen.spec import CodegenError, SdkNaming, load # noqa: E402 + +NOW = dt.datetime(2026, 10, 18, 6, 0, tzinfo=dt.timezone.utc) + + +@pytest.fixture +def openapi(tmp_path: pathlib.Path) -> pathlib.Path: + target = tmp_path / "openapi" + shutil.copytree(REPO / "openapi", target) + return target + + +def _read(path: pathlib.Path) -> Any: + return json.loads(path.read_text("utf-8")) + + +def _write(path: pathlib.Path, value: Any) -> None: + path.write_text(json.dumps(value, indent=1, ensure_ascii=False), "utf-8") + + +def _spec_operation(openapi_dir: pathlib.Path, key: str) -> tuple[pathlib.Path, Any, Any]: + """The spec file, its document and the operation behind an overlay key.""" + file = openapi_dir / ("account.json" if key.startswith("account ") else "verification.json") + http_method, path = key.removeprefix("account ").split(" ", 1) + doc = _read(file) + return file, doc, doc["paths"][path][http_method.lower()] + + +def _naming(openapi_dir: pathlib.Path, key: str) -> Any: + """An operation's naming: its own x-didit-sdk (upstream wins), else the overlay entry. + + Tests never assume where the naming lives: once upstream carries x-didit-sdk, the spec + sync drops the overlay entries (DESIGN section 3). + """ + _, _, op = _spec_operation(openapi_dir, key) + return op.get("x-didit-sdk") or _read(openapi_dir / "overlay.json")["operations"][key] + + +def _change_naming(openapi_dir: pathlib.Path, key: str, **changes: Any) -> None: + """Change an operation's naming where it lives (upstream x-didit-sdk, else the overlay).""" + file, doc, op = _spec_operation(openapi_dir, key) + if op.get("x-didit-sdk"): + op["x-didit-sdk"].update(changes) + _write(file, doc) + else: + overlay = _read(openapi_dir / "overlay.json") + overlay["operations"][key].update(changes) + _write(openapi_dir / "overlay.json", overlay) + + +# Naming ----------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("text", "pascal", "snake"), + [ + ("key_people_checks", "KeyPeopleChecks", "key_people_checks"), + ("sessionId", "SessionId", "session_id"), + ("IDVerification", "IdVerification", "id_verification"), + ("Idempotency-Key", "IdempotencyKey", "idempotency_key"), + ("aml_v2", "AmlV2", "aml_v2"), + ("2fa", "V2fa", "v_2fa"), + ("", "Value", "value"), + ], +) +def test_case_conversions(text: str, pascal: str, snake: str) -> None: + assert naming.pascal(text) == pascal + assert naming.snake(text) == snake + + +def test_identifiers() -> None: + assert naming.identifier("from") == "from_" + assert naming.identifier("_private") == "private" + assert naming.identifier("first-name") == "first_name" + assert naming.identifier("été") == "t" + assert naming.identifier("json", taken=frozenset({"json"})) == "json_" + assert naming.resource_class("cases.routing_rules") == "CasesRoutingRules" + + +def test_sdk_naming_validation() -> None: + ok = SdkNaming.parse( + "k", {"resource": "a.b", "method": "list", "auth": "api_key", "server": True} + ) + assert ok.as_json() == {"auth": "api_key", "method": "list", "resource": "a.b", "server": True} + bad: list[dict[str, Any]] = [ + {"resource": "A", "method": "list", "auth": "api_key", "server": True}, + {"resource": "a", "method": "List", "auth": "api_key", "server": True}, + {"resource": "a", "method": "list", "auth": "oauth", "server": True}, + {"resource": "a", "method": "list", "auth": "api_key", "server": "yes"}, + {"resource": "a", "method": "list", "auth": "api_key", "server": True, "scope": "org"}, + {"resource": "a", "method": "list", "auth": "api_key", "server": True, "extra": 1}, + {"resource": "a", "method": "list", "auth": "api_key"}, + ] + for raw in bad: + with pytest.raises(CodegenError): + SdkNaming.parse("k", raw) + with pytest.raises(CodegenError): + SdkNaming.parse("k", ["not", "an", "object"]) + + +# Loading ------------------------------------------------------------------------------ + + +def _count_operations(openapi_dir: pathlib.Path) -> tuple[int, int]: + """All operations and the server ones, counted straight from the JSON.""" + overlay = _read(openapi_dir / "overlay.json")["operations"] + total = server = 0 + for name, prefix in (("verification.json", ""), ("account.json", "account ")): + for path, item in _read(openapi_dir / name)["paths"].items(): + for method in ("get", "put", "post", "delete", "patch", "head", "options"): + if method in item: + total += 1 + naming = ( + item[method].get("x-didit-sdk") + or overlay[f"{prefix}{method.upper()} {path}"] + ) + server += bool(naming["server"]) + deprecated = len(_read(openapi_dir / "deprecated-operations.json")["methods"]) + return total + deprecated, server + deprecated + + +def test_repository_specs_load() -> None: + model = load(REPO / "openapi") + total, server = _count_operations(REPO / "openapi") + assert len(model.operations) == total + assert len(model.server_operations) == server + assert {op.api for op in model.server_operations} == {"verification", "account"} + + +def test_new_operation_without_naming_fails_with_its_name(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + spec["paths"]["/v3/brand-new/"] = { + "get": {"operationId": "brand_new_list", "responses": {"200": {"description": "ok"}}} + } + _write(openapi / "verification.json", spec) + with pytest.raises(CodegenError) as info: + load(openapi) + assert "GET /v3/brand-new/ (operationId brand_new_list)" in str(info.value) + assert "openapi/overlay.json" in str(info.value) + + +def test_upstream_naming_wins_over_the_overlay(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + spec["paths"]["/v3/session/"]["post"]["x-didit-sdk"] = { + "resource": "sessions", + "method": "start", + "auth": "api_key", + "server": True, + } + _write(openapi / "verification.json", spec) + op = next(o for o in load(openapi).operations if o.key == "POST /v3/session/") + assert op.sdk.method == "start" + + +def test_stale_overlay_entry_fails(openapi: pathlib.Path) -> None: + overlay = _read(openapi / "overlay.json") + overlay["operations"]["GET /v3/gone/"] = { + "resource": "gone", + "method": "list", + "auth": "api_key", + "server": True, + } + _write(openapi / "overlay.json", overlay) + with pytest.raises(CodegenError, match="no longer exist upstream"): + load(openapi) + + +def test_duplicate_method_names_fail(openapi: pathlib.Path) -> None: + _change_naming(openapi, "GET /v3/sessions/", method="create") + with pytest.raises(CodegenError, match=r"both map to sessions\.create"): + load(openapi) + + +def test_method_clashing_with_a_nested_resource_fails(openapi: pathlib.Path) -> None: + _change_naming(openapi, "GET /v3/sessions/", method="imports") + with pytest.raises(CodegenError, match="clashes with the nested resource"): + load(openapi) + + +def test_application_scope_needs_the_prefix(openapi: pathlib.Path) -> None: + _change_naming(openapi, "GET /v3/sessions/", scope="application") + with pytest.raises(CodegenError, match="scope application"): + load(openapi) + + +# Generation --------------------------------------------------------------------------- + + +def test_generation_is_deterministic_and_marked() -> None: + first = generate(REPO / "openapi") + second = generate(REPO / "openapi") + assert first == second + assert all(path.startswith(GENERATED_ROOTS) for path in first) + for path, text in first.items(): + assert text.startswith(HEADER), path + + +def test_every_server_operation_becomes_one_method() -> None: + model, builder, methods = build(REPO / "openapi") + assert len(methods) == len(model.server_operations) == _count_operations(REPO / "openapi")[1] + kinds = {m.response.kind for m in methods} + assert kinds == {"model", "page", "bytes", "none"} + # Counted from the JSON, independently of the generator. + pages = multipart = scoped = 0 + + def binary(schema: Any, spec: Any) -> set[str]: + props = spec.resolve(schema).get("properties", {}) + out = set() + for name, prop in props.items(): + prop = spec.resolve(prop) + item = spec.resolve(prop.get("items", {})) + if "binary" in (prop.get("format"), item.get("format")): + out.add(name) + return out + + for op in model.server_operations: + props = ( + op.spec.resolve( + ((op.op.get("responses") or {}).get("200") or {}) + .get("content", {}) + .get("application/json", {}) + .get("schema", {}) + ) + or {} + ).get("properties", {}) + pages += {"count", "next", "previous", "results"} <= set(props) + content = (op.op.get("requestBody") or {}).get("content", {}) + if "multipart/form-data" in content: + files = binary(content["multipart/form-data"].get("schema", {}), op.spec) + if "application/json" in content: + json_schema = op.spec.resolve(content["application/json"].get("schema", {})) + files -= set(json_schema.get("properties", {})) + multipart += bool(files) + scoped += op.sdk.scope == "application" + assert sum(1 for m in methods if m.response.kind == "page") == pages + assert sum(1 for m in methods if m.body_mode in ("multipart", "auto")) == multipart + assert sum(1 for m in methods if m.op.sdk.scope == "application") == scoped + assert pages + assert multipart + assert scoped + names = set(builder.registry.defs) + assert "SessionsCreateParams" in names + assert "SessionsCreateParamsContactDetails" in names + assert "SessionsCreateResponse" in names + assert "TransactionDetail" in names + + +def test_query_parameter_clashing_with_a_body_field_is_renamed() -> None: + _, _, methods = build(REPO / "openapi") + method = next( + m for m in methods if m.resource == "sessions" and m.name == "update_aml_hit_status" + ) + by_py = {a.py: a for a in method.kwargs} + assert by_py["node_id"].location == "body" + assert by_py["node_id_query"].location == "query" + assert by_py["node_id_query"].wire == "node_id" + + +def test_removed_operation_stays_as_a_deprecated_method(openapi: pathlib.Path) -> None: + upstream = openapi.parent / "upstream" + upstream.mkdir() + spec = _read(openapi / "verification.json") + del spec["paths"]["/v3/billing/balance/"] + (upstream / "verification.json").write_text(json.dumps(spec), "utf-8") + shutil.copy(openapi / "account.json", upstream / "account.json") + + downloads = {name: (upstream / name).read_bytes() for name in spec_sync.UPSTREAM} + changed, summary = spec_sync.sync( + openapi, downloads, today=NOW.date(), now=NOW, version="0.4.2" + ) + assert changed + assert "billing.retrieve_balance` was removed upstream" in summary + assert "## Breaking changes" in summary + deprecated = _read(openapi / "deprecated-operations.json")["methods"] + entry = deprecated["billing.retrieve_balance"] + assert entry["reason"] == "removed" + assert entry["operation_key"] == "GET /v3/billing/balance/" + assert entry["since"] == "2026-10-18" + assert entry["deprecated_since"] == "0.4.2" + assert entry["remove_in"] == "0.5.0" + assert "$ref" not in json.dumps(deprecated) + assert "GET /v3/billing/balance/" not in _read(openapi / "overlay.json")["operations"] + + files = generate(openapi) + billing = files["src/didit/resources/billing.py"] + assert "@deprecated(" in billing + assert "def retrieve_balance(" in billing + assert "removed in version 0.5.0" in billing + + +def test_renamed_method_stays_as_a_deprecated_alias(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + spec["paths"]["/v3/billing/balance/"]["get"]["x-didit-sdk"] = { + **_naming(openapi, "GET /v3/billing/balance/"), + "method": "get_balance", + } + downloads = { + "verification.json": json.dumps(spec).encode(), + "account.json": (openapi / "account.json").read_bytes(), + } + changed, summary = spec_sync.sync( + openapi, downloads, today=NOW.date(), now=NOW, version="0.1.0" + ) + assert changed + breaking = summary.split("## Breaking changes", 1)[1].split("##", 1)[0] + assert "`billing.retrieve_balance` was renamed upstream to `billing.get_balance`" in breaking + assert "until version 0.2.0" in breaking + entry = _read(openapi / "deprecated-operations.json")["methods"]["billing.retrieve_balance"] + assert entry["reason"] == "renamed" + assert entry["replacement"] == "billing.get_balance" + + files = generate(openapi) + billing = files["src/didit/resources/billing.py"] + assert "def get_balance(" in billing + assert "def retrieve_balance(" in billing + assert "Renamed to billing.get_balance" in billing + # The old response type stays importable next to the new one. + types = "".join(t for p, t in files.items() if p.startswith("src/didit/types/")) + assert "class BillingRetrieveBalanceResponse(" in types + assert "class BillingGetBalanceResponse(" in types + + # Renamed back: the old name is live again (its alias is dropped), and the + # interim name it shipped under becomes the deprecated alias in turn. + spec["paths"]["/v3/billing/balance/"]["get"]["x-didit-sdk"]["method"] = "retrieve_balance" + downloads["verification.json"] = json.dumps(spec).encode() + _, summary = spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW, version="0.1.1") + assert "`billing.retrieve_balance` is provided again" in summary + methods = _read(openapi / "deprecated-operations.json")["methods"] + assert list(methods) == ["billing.get_balance"] + assert methods["billing.get_balance"]["replacement"] == "billing.retrieve_balance" + load(openapi) + + +def _sync(openapi: pathlib.Path, spec: Any) -> str: + downloads = { + "verification.json": json.dumps(spec).encode(), + "account.json": (openapi / "account.json").read_bytes(), + } + _, summary = spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW, version="0.1.0") + return summary + + +def _breaking(summary: str) -> str: + return summary.split("## Breaking changes", 1)[1].split("\n## ", 1)[0] + + +def _name_op(openapi: pathlib.Path, spec: Any, path: str, http_method: str, **naming: Any) -> None: + key = f"{http_method.upper()} {path}" + spec["paths"][path][http_method]["x-didit-sdk"] = {**_naming(openapi, key), **naming} + + +def test_swapped_names_are_breaking(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + _name_op(openapi, spec, "/v3/billing/balance/", "get", method="top_up") + _name_op(openapi, spec, "/v3/billing/top-up/", "post", method="retrieve_balance") + breaking = _breaking(_sync(openapi, spec)) + assert "`billing.top_up` now calls `GET /v3/billing/balance/` instead of" in breaking + assert "`billing.retrieve_balance` now calls `POST /v3/billing/top-up/`" in breaking + assert _read(openapi / "deprecated-operations.json")["methods"] == {} + + +def test_a_name_reused_by_another_operation_is_breaking(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + _name_op(openapi, spec, "/v3/billing/balance/", "get", method="current_balance") + spec["paths"]["/v3/billing/balance-v2/"] = { + "get": { + "responses": {"200": {"description": "ok"}}, + "x-didit-sdk": {**_naming(openapi, "GET /v3/billing/balance/")}, + } + } + breaking = _breaking(_sync(openapi, spec)) + assert ( + "`billing.retrieve_balance` now calls `GET /v3/billing/balance-v2/` instead of " + "`GET /v3/billing/balance/`" in breaking + ) + + +def test_a_moved_operation_reports_what_changed(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + item = spec["paths"].pop("/v3/billing/balance/") + item["get"]["x-didit-sdk"] = _naming(openapi, "GET /v3/billing/balance/") + item["get"]["parameters"] = [ + {"name": "currency", "in": "query", "required": True, "schema": {"type": "string"}} + ] + item["get"]["responses"]["200"]["content"]["application/json"]["schema"] = { + "type": "array", + "items": {"type": "object"}, + } + spec["paths"]["/v4/billing/balance/"] = item + summary = _sync(openapi, spec) + breaking = _breaking(summary) + assert "`billing.retrieve_balance` moved from `GET /v3/billing/balance/`" in breaking + assert "parameter added: currency (query, required)" in breaking + assert "response: type object -> array" in breaking + assert _read(openapi / "deprecated-operations.json")["methods"] == {} + + +def test_an_operation_that_stops_being_a_server_operation_is_removed( + openapi: pathlib.Path, +) -> None: + spec = _read(openapi / "verification.json") + _name_op(openapi, spec, "/v3/billing/balance/", "get", auth="device_token", server=False) + breaking = _breaking(_sync(openapi, spec)) + assert "`billing.retrieve_balance` (GET /v3/billing/balance/) is removed" in breaking + # Never kept: server SDKs do not expose device-token operations (DESIGN 4). + assert _read(openapi / "deprecated-operations.json")["methods"] == {} + files = generate(openapi) + assert "def retrieve_balance(" not in files["src/didit/resources/billing.py"] + + +def test_an_operation_that_loses_its_naming_is_called_out(openapi: pathlib.Path) -> None: + # Upstream named the operation (so the overlay no longer does), then drops it. + naming = _naming(openapi, "GET /v3/billing/balance/") + old = _read(openapi / "verification.json") + overlay = _read(openapi / "overlay.json") + overlay["operations"].pop("GET /v3/billing/balance/", None) + old["paths"]["/v3/billing/balance/"]["get"]["x-didit-sdk"] = naming + _write(openapi / "verification.json", old) + _write(openapi / "overlay.json", overlay) + spec = _read(openapi / "verification.json") + del spec["paths"]["/v3/billing/balance/"]["get"]["x-didit-sdk"] + summary = _sync(openapi, spec) + assert "lost its SDK naming" in summary + assert "`billing.retrieve_balance` (GET /v3/billing/balance/) has no SDK naming" in _breaking( + summary + ) + assert _read(openapi / "deprecated-operations.json")["methods"] == {} + with pytest.raises(CodegenError, match="no SDK naming"): + load(openapi) + + +def test_nested_type_and_enum_changes_are_breaking(openapi: pathlib.Path) -> None: + old = _read(openapi / "verification.json") + old["components"]["schemas"]["LegacyUnusedThing"] = {"type": "object"} + _write(openapi / "verification.json", old) + spec = _read(openapi / "verification.json") + del spec["components"]["schemas"]["LegacyUnusedThing"] + schemas = spec["components"]["schemas"] + # The reviewer's case: a nested component property changes type. + schemas["DocumentAIDetectedCode"]["properties"]["type"] = {"type": "integer"} + prop = "type" + downloads = { + "verification.json": json.dumps(spec).encode(), + "account.json": (openapi / "account.json").read_bytes(), + } + _, summary = spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW, version="0.1.0") + breaking = summary.split("## Breaking changes", 1)[1].split("## ", 1)[0] + assert f".{prop}: type string -> integer (breaking)" in breaking + assert "schema `LegacyUnusedThing` removed" in breaking + + +def test_schema_diff_rules() -> None: + def diff(old: Any, new: Any, *, request: bool) -> list[tuple[str, bool]]: + notes: list[tuple[str, bool]] = [] + spec_sync.diff_schema(old, new, "x", request=request, notes=notes) + return notes + + status = {"type": "string", "enum": ["a", "b"]} + assert diff(status, {**status, "enum": ["a"]}, request=True) == [ + ('x: values removed ["b"]', True) + ] + assert diff(status, {**status, "enum": ["a"]}, request=False) == [ + ('x: values removed ["b"]', False) + ] + obj = {"type": "object", "properties": {"a": {"type": "string"}}, "required": ["a"]} + assert diff(obj, {**obj, "required": []}, request=False) == [ + ("x.a: no longer always present", True) + ] + assert diff({**obj, "required": []}, obj, request=True) == [("x.a: now required", True)] + assert diff({"type": "integer"}, {"type": "number"}, request=True) == [ + ("x: type integer -> number", False) + ] + assert diff({"type": "string"}, {"type": "string", "nullable": True}, request=False) == [ + ("x: can now be null", True) + ] + nested = {"type": "array", "items": obj} + assert diff( + nested, {"type": "array", "items": {"type": "object", "properties": {}}}, request=False + ) == [("x[].a: removed", True)] + assert diff({"oneOf": [obj, status]}, {"oneOf": [obj]}, request=False) == [ + ("x: 1 oneOf variant(s) removed", True) + ] + documented = {**obj, "description": "now documented", "example": {"a": "z"}} + assert diff(obj, documented, request=False) == [] + + +@pytest.mark.parametrize( + "body", + [ + b"maintenance", + b'{"message": "maintenance"}', + b'{"openapi": "3.1.0", "paths": {}}', + b"[]", + ], +) +def test_sync_refuses_a_download_that_is_not_openapi(openapi: pathlib.Path, body: bytes) -> None: + before = {p.name: p.read_bytes() for p in openapi.iterdir()} + downloads = { + "verification.json": (openapi / "verification.json").read_bytes(), + "account.json": body, + } + with pytest.raises(spec_sync.SpecSyncError, match=r"account\.json"): + spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW) + assert {p.name: p.read_bytes() for p in openapi.iterdir()} == before + + +def test_deprecated_entry_that_is_provided_again_names_the_fix(openapi: pathlib.Path) -> None: + deprecated = _read(openapi / "deprecated-operations.json") + deprecated["methods"]["billing.retrieve_balance"] = { + "reason": "removed", + "operation_key": "GET /v3/billing/balance-old/", + "x-didit-sdk": _naming(openapi, "GET /v3/billing/balance/"), + "operation": {"responses": {"200": {"description": "ok"}}}, + "remove_in": "0.2.0", + } + _write(openapi / "deprecated-operations.json", deprecated) + with pytest.raises( + CodegenError, match=r"delete this entry from openapi/deprecated-operations\.json" + ): + load(openapi) + + +def test_next_minor() -> None: + assert spec_sync.next_minor("0.1.0") == "0.2.0" + assert spec_sync.next_minor("1.9.3") == "1.10.0" + assert spec_sync.next_minor("garbage") == "0.1.0" + + +def test_moved_operation_is_not_deprecated(openapi: pathlib.Path) -> None: + spec = _read(openapi / "verification.json") + spec["paths"]["/v3/billing/balance-v2/"] = spec["paths"].pop("/v3/billing/balance/") + entry = _naming(openapi, "GET /v3/billing/balance/") + spec["paths"]["/v3/billing/balance-v2/"]["get"]["x-didit-sdk"] = entry + downloads = { + "verification.json": json.dumps(spec).encode(), + "account.json": (openapi / "account.json").read_bytes(), + } + changed, summary = spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW) + assert changed + assert "moved to `GET /v3/billing/balance-v2/`" in summary + assert _read(openapi / "deprecated-operations.json")["methods"] == {} + load(openapi) + + +def test_sync_reports_changes_and_drops_overlay_entries(openapi: pathlib.Path) -> None: + # Before the sync only the overlay names the operation; upstream then carries it. + naming = _naming(openapi, "POST /v3/session/") + old = _read(openapi / "verification.json") + old["paths"]["/v3/session/"]["post"].pop("x-didit-sdk", None) + _write(openapi / "verification.json", old) + overlay = _read(openapi / "overlay.json") + overlay["operations"]["POST /v3/session/"] = naming + _write(openapi / "overlay.json", overlay) + spec = _read(openapi / "verification.json") + op = spec["paths"]["/v3/session/"]["post"] + op["x-didit-sdk"] = naming + op["parameters"] = [ + {"name": "dry_run", "in": "query", "required": True, "schema": {"type": "boolean"}} + ] + spec["paths"]["/v3/new-thing/"] = {"get": {"responses": {"200": {"description": "ok"}}}} + downloads = { + "verification.json": json.dumps(spec).encode(), + "account.json": (openapi / "account.json").read_bytes(), + } + changed, summary = spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW) + assert changed + assert "`GET /v3/new-thing/`: **no SDK naming**" in summary + assert "parameter added: dry_run (query, required)" in summary + assert "**(breaking)**" in summary + assert "Dropped 1 overlay entries" in summary + assert "POST /v3/session/" not in _read(openapi / "overlay.json")["operations"] + source = _read(openapi / "SOURCE.json") + assert source["verification.json"]["fetched_at"] == "2026-10-18T06:00:00Z" + assert len(source["verification.json"]["sha256"]) == 64 + assert source["webhook-vectors.json"]["generator"] + + +def test_sync_without_changes_touches_nothing(openapi: pathlib.Path) -> None: + before = {p.name: p.read_bytes() for p in openapi.iterdir()} + downloads = {name: (openapi / name).read_bytes() for name in spec_sync.UPSTREAM} + changed, summary = spec_sync.sync(openapi, downloads, today=NOW.date(), now=NOW) + assert not changed + assert summary == "No upstream change.\n" + assert {p.name: p.read_bytes() for p in openapi.iterdir()} == before + + +def test_seeded_json_layout_round_trips() -> None: + for name in ("overlay.json", "SOURCE.json"): + raw = (REPO / "openapi" / name).read_text("utf-8") + assert json.dumps(json.loads(raw), indent=1, ensure_ascii=False) == raw + + +def test_cli_main_from_dir( + monkeypatch: pytest.MonkeyPatch, openapi: pathlib.Path, tmp_path: pathlib.Path +) -> None: + output = tmp_path / "github_output" + summary = tmp_path / "summary.md" + monkeypatch.setattr(spec_sync, "REPO", openapi.parent) + monkeypatch.setenv("GITHUB_OUTPUT", str(output)) + assert spec_sync.main(["--from-dir", str(openapi), "--summary", str(summary)]) == 0 + assert "changed=false" in output.read_text("utf-8") + assert summary.read_text("utf-8") == "No upstream change.\n" diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..7c94d7e --- /dev/null +++ b/examples/README.md @@ -0,0 +1,26 @@ +# Examples + +Each script reads the API key from `DIDIT_API_KEY` (Business Console, Developers, API keys). +Use a sandbox application while you try them. + +| Script | What it shows | +|---|---| +| `quickstart.py` | Create a KYC session, read its decision, verify a webhook (needs `DIDIT_WORKFLOW_ID`) | +| `async_quickstart.py` | The same with `AsyncDidit`, and `async for` auto-pagination | +| `id_verification.py` | Standalone ID verification with file uploads | +| `aml_screening.py` | AML screening with an idempotency key, and error handling | +| `transaction_screening.py` | Transactions (KYT): submit one, then list with auto-pagination | +| `travel_rule.py` | Travel Rule settings, VASP directory, wallet addresses, wallet screening | +| `authentication.py` | Verify a Didit Authentication access token in your backend (needs `didit-sdk[auth]`) | +| `webhooks/fastapi_app.py` | Webhook receiver for FastAPI | +| `webhooks/flask_app.py` | Webhook receiver for Flask | +| `webhooks/django_app.py` | Webhook receiver for Django (single file) | + +``` +pip install didit-sdk +python examples/quickstart.py +``` + +The webhook receivers need their framework (`pip install fastapi uvicorn`, `flask` or `django`) +and `DIDIT_WEBHOOK_SECRET`, the secret of the webhook destination. Always pass the raw request +body to `didit.webhooks.verify`; verify before you parse. diff --git a/examples/aml_screening.py b/examples/aml_screening.py new file mode 100644 index 0000000..9bd41db --- /dev/null +++ b/examples/aml_screening.py @@ -0,0 +1,25 @@ +"""AML screening of a person (sanctions, PEP, adverse media). + +python examples/aml_screening.py "Maria Garcia" +""" + +from __future__ import annotations + +import sys + +import didit + +client = didit.Didit() + +try: + result = client.aml.screen( + full_name=sys.argv[1] if len(sys.argv) > 1 else "Maria Garcia", + date_of_birth="1992-03-14", + nationality="ESP", + vendor_data="user-123", + idempotency_key="aml-user-123-2026-10-17", # a retry with the same key is not billed twice + ) +except didit.RateLimitError as err: + print("Slow down; request id", err.request_id) + raise +print(result.to_json()) diff --git a/examples/async_quickstart.py b/examples/async_quickstart.py new file mode 100644 index 0000000..1f6c45d --- /dev/null +++ b/examples/async_quickstart.py @@ -0,0 +1,27 @@ +"""The same calls on AsyncDidit, plus auto-pagination with ``async for``. + +python examples/async_quickstart.py +""" + +from __future__ import annotations + +import asyncio +import os + +from didit import AsyncDidit + + +async def main() -> None: + async with AsyncDidit(base_url=os.environ.get("DIDIT_BASE_URL")) as client: + session = await client.sessions.create( + workflow_id=os.environ["DIDIT_WORKFLOW_ID"], vendor_data="user-123" + ) + print("Session URL:", session.url) + + # Every session of the last week, across all pages. + async for row in client.sessions.list(date_from="2026-10-10", limit=50): + print(row.session_id, row.status) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/authentication.py b/examples/authentication.py new file mode 100644 index 0000000..3096f9e --- /dev/null +++ b/examples/authentication.py @@ -0,0 +1,56 @@ +"""Didit Authentication in your backend: verify the access token on every API call, +and gate a sensitive action behind a step-up. + + pip install "didit-sdk[auth]" + export DIDIT_PUBLISHABLE_KEY=pk_test_... # from the Authentication settings + export DIDIT_APPLICATION_ID=... # your application id (the OAuth client_id) + python examples/authentication.py +""" + +from __future__ import annotations + +import os +import sys + +from didit.authentication import AccessTokenClaims, Authentication, TokenVerificationError + +# One instance per process: it caches the issuer's discovery document and keys. +auth = Authentication.from_publishable_key( + os.environ["DIDIT_PUBLISHABLE_KEY"], + client_id=os.environ["DIDIT_APPLICATION_ID"], +) + + +def current_user(authorization_header: str) -> AccessTokenClaims: + """What an API route of yours does with ``Authorization: Bearer ``.""" + scheme, _, token = authorization_header.partition(" ") + if scheme.lower() != "bearer" or not token: + raise PermissionError("missing bearer token") + try: + return auth.verify_access_token(token) + except TokenVerificationError as err: + raise PermissionError(f"invalid token: {err.reason}") from err + + +def approve_payout(access_token: str, amount: str) -> str: + """Ask Didit whether this payout needs a fresh verification first.""" + step_up = auth.create_step_up( + access_token, + action="payout", + amount=amount, + currency="EUR", + return_to="https://example.com/payouts/confirm", + ) + # Fail closed: only an explicit "required": false lets the payout go ahead. + if step_up.required is False: + return "go ahead" + # Send the person to step_up.verification_url. When they come back, read the + # step-up again and act only on status "approved". + return f"verify first: {step_up.verification_url}" + + +if __name__ == "__main__": + claims = current_user(f"Bearer {sys.argv[1]}") + print("user", claims.sub, "session", claims.sid, "vendor_data", claims.vendor_data) + # get_session() asks the issuer, so it also catches sessions revoked a moment ago. + print("session still active:", auth.get_session(sys.argv[1]).model_dump(exclude_none=True)) diff --git a/examples/id_verification.py b/examples/id_verification.py new file mode 100644 index 0000000..da29914 --- /dev/null +++ b/examples/id_verification.py @@ -0,0 +1,21 @@ +"""Standalone ID verification: upload the two sides of a document. + +python examples/id_verification.py front.jpg back.jpg +""" + +from __future__ import annotations + +import pathlib +import sys + +import didit + +client = didit.Didit() + +front, back = (pathlib.Path(p) for p in sys.argv[1:3]) +result = client.id_verification.verify( + front_image=front, # a path, bytes, an open binary file, or (filename, content, content_type) + back_image=back, + vendor_data="user-123", +) +print(result.to_json()) diff --git a/examples/quickstart.py b/examples/quickstart.py new file mode 100644 index 0000000..008181e --- /dev/null +++ b/examples/quickstart.py @@ -0,0 +1,50 @@ +"""Quickstart: create a verification session, read its decision, verify a webhook. + +export DIDIT_API_KEY=... # Business Console > Developers > API keys +export DIDIT_WORKFLOW_ID=... # a published workflow +python examples/quickstart.py +""" + +from __future__ import annotations + +import hashlib +import hmac +import json +import os +import time + +import didit +from didit import Didit + +client = Didit(base_url=os.environ.get("DIDIT_BASE_URL")) # api key from DIDIT_API_KEY + +# 1. Create a session and send the user to its hosted URL. +session = client.sessions.create( + workflow_id=os.environ["DIDIT_WORKFLOW_ID"], + vendor_data="user-123", + callback="https://example.com/verification/done", +) +print("Send the user to:", session.url) + +# 2. Later (or from the status.updated webhook), read the decision. +decision = client.sessions.retrieve_decision(session.session_id) +print("Status:", decision.status) + +# 3. Verify a webhook delivery. Here we sign a sample body ourselves; in your app the +# body and headers come from the request (see examples/webhooks/). +secret = os.environ.get("DIDIT_WEBHOOK_SECRET", "whsec_test_quickstart_example") +body = json.dumps( + { + "session_id": session.session_id, + "status": "Approved", + "webhook_type": "status.updated", + "timestamp": int(time.time()), + }, + sort_keys=True, + separators=(",", ":"), +).encode() +headers = {"X-Signature": hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()} + +event = didit.webhooks.verify(body, headers, secret) +if isinstance(event, didit.webhooks.SessionStatusUpdatedEvent): + print("Webhook verified:", event.session_id, event.status) diff --git a/examples/transaction_screening.py b/examples/transaction_screening.py new file mode 100644 index 0000000..4c9ec42 --- /dev/null +++ b/examples/transaction_screening.py @@ -0,0 +1,27 @@ +"""Transaction monitoring (KYT): submit a transaction, then walk the latest ones. + +python examples/transaction_screening.py +""" + +from __future__ import annotations + +import didit + +client = didit.Didit() + +transaction = client.transactions.create( + transaction_id="your-txn-2026-05-17-001", + transaction_category="finance", + transaction_details={ + "direction": "outbound", + "amount": 1500, + "currency": "EUR", + "action_type": "withdrawal", + }, + subject={"entity_type": "individual", "vendor_data": "user-12345", "full_name": "Maria Garcia"}, +) +print("Status:", transaction.status, "score:", transaction.score) + +# Newest first; iterating the page fetches the following pages too. +for row in client.transactions.list(limit=50): + print(row.uuid, row.status) diff --git a/examples/travel_rule.py b/examples/travel_rule.py new file mode 100644 index 0000000..f972a2e --- /dev/null +++ b/examples/travel_rule.py @@ -0,0 +1,26 @@ +"""Travel Rule: settings, the VASP directory and your own wallet addresses. + +python examples/travel_rule.py +""" + +from __future__ import annotations + +import didit + +client = didit.Didit() + +settings = client.travel_rule.retrieve_settings() +print(settings.to_json()) + +for vasp in client.travel_rule.list_vasps(search="exchange", limit=20): + print("VASP:", vasp.name) + +address = client.travel_rule.wallet_addresses.create( + address="bc1qexampleexchangewalletaddressxyz", chain="BTC" +) +print("Registered wallet address", address.uuid) + +screening = client.wallet_screening.screen( + wallet_address="bc1qexampleexternalwalletaddressabc", blockchain="BTC" +) +print("Wallet risk:", screening.to_json()) diff --git a/examples/webhooks/django_app.py b/examples/webhooks/django_app.py new file mode 100644 index 0000000..0ef7e07 --- /dev/null +++ b/examples/webhooks/django_app.py @@ -0,0 +1,47 @@ +"""Didit webhook receiver for Django, as a single runnable file. + + pip install didit-sdk django + DIDIT_WEBHOOK_SECRET=... python examples/webhooks/django_app.py runserver 8000 + +In a real project, put ``didit_webhook`` in your app's views.py and route it in urls.py. +""" + +from __future__ import annotations + +import os +import sys + +from django.conf import settings +from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest +from django.urls import path +from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_POST + +import didit + +settings.configure( + DEBUG=False, + ALLOWED_HOSTS=["localhost", "127.0.0.1"], + ROOT_URLCONF=__name__, + SECRET_KEY=os.environ.get("DJANGO_SECRET_KEY", "change-me"), +) +SECRET = os.environ["DIDIT_WEBHOOK_SECRET"] + + +@csrf_exempt +@require_POST +def didit_webhook(request: HttpRequest) -> HttpResponse: + try: + event = didit.webhooks.verify(request.body, request.headers, SECRET) + except didit.WebhookVerificationError as err: + return HttpResponseBadRequest(err.reason) + print("received", event.webhook_type, event.event_id) + return HttpResponse(status=204) + + +urlpatterns = [path("webhooks/didit", didit_webhook)] + +if __name__ == "__main__": + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/examples/webhooks/fastapi_app.py b/examples/webhooks/fastapi_app.py new file mode 100644 index 0000000..1e35571 --- /dev/null +++ b/examples/webhooks/fastapi_app.py @@ -0,0 +1,33 @@ +"""Didit webhook receiver for FastAPI. + +pip install didit-sdk fastapi uvicorn +DIDIT_WEBHOOK_SECRET=... uvicorn examples.webhooks.fastapi_app:app --port 8000 +""" + +from __future__ import annotations + +import os + +from fastapi import FastAPI, HTTPException, Request + +import didit +from didit.webhooks import SessionStatusUpdatedEvent, TransactionStatusUpdatedEvent + +app = FastAPI() +SECRET = os.environ["DIDIT_WEBHOOK_SECRET"] + + +@app.post("/webhooks/didit") +async def didit_webhook(request: Request) -> dict[str, str]: + body = await request.body() # the raw bytes, before any JSON parsing + try: + event = didit.webhooks.verify(body, request.headers, SECRET) + except didit.WebhookVerificationError as err: + raise HTTPException(status_code=400, detail=err.reason) from err + + if isinstance(event, SessionStatusUpdatedEvent): + print("session", event.session_id, "is now", event.status) + elif isinstance(event, TransactionStatusUpdatedEvent): + print("transaction", event.transaction_id, "is now", event.status) + # Answer 2xx quickly; do slow work in a background job keyed by event.event_id. + return {"received": "ok"} diff --git a/examples/webhooks/flask_app.py b/examples/webhooks/flask_app.py new file mode 100644 index 0000000..3abaeee --- /dev/null +++ b/examples/webhooks/flask_app.py @@ -0,0 +1,26 @@ +"""Didit webhook receiver for Flask. + +pip install didit-sdk flask +DIDIT_WEBHOOK_SECRET=... flask --app examples/webhooks/flask_app.py run --port 8000 +""" + +from __future__ import annotations + +import os + +from flask import Flask, abort, request + +import didit + +app = Flask(__name__) +SECRET = os.environ["DIDIT_WEBHOOK_SECRET"] + + +@app.post("/webhooks/didit") +def didit_webhook() -> tuple[str, int]: + try: + event = didit.webhooks.verify(request.get_data(), request.headers, SECRET) + except didit.WebhookVerificationError as err: + abort(400, err.reason) + print("received", event.webhook_type, event.event_id) + return "", 204 diff --git a/openapi/deprecated-operations.json b/openapi/deprecated-operations.json new file mode 100644 index 0000000..f29032f --- /dev/null +++ b/openapi/deprecated-operations.json @@ -0,0 +1,4 @@ +{ + "description": "Public SDK methods kept for one more minor version after the API contract dropped them (docs/DESIGN.md section 3.1). Written by codegen/spec_sync.py, keyed by resource.method. reason is removed (the operation left the contract) or renamed (its x-didit-sdk naming changed; replacement names the new method). Each entry holds the fully dereferenced operation, so it never depends on the current documents. tests/test_contract.py fails once the package version reaches remove_in: delete the entry and regenerate in that release.", + "methods": {} +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6afff61 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,157 @@ +[project] +name = "didit-sdk" +version = "0.1.0" +description = "The official Didit Python library: identity verification (KYC), business verification (KYB), AML, liveness, transaction monitoring, Travel Rule and customer authentication." +readme = "README.md" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Didit", email = "support@didit.me" }] +requires-python = ">=3.10" +dependencies = [ + "httpx>=0.27,<1", + "pydantic>=2.7,<3", + "typing-extensions>=4.10", +] +keywords = [ + "didit", + "identity verification", + "kyc", + "kyb", + "aml", + "liveness", + "transaction monitoring", + "travel rule", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Security", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] + +[project.optional-dependencies] +auth = ["cryptography>=42"] + +[project.urls] +Homepage = "https://didit.me" +Documentation = "https://docs.didit.me" +Repository = "https://github.com/didit-protocol/sdk-python" +Issues = "https://github.com/didit-protocol/sdk-python/issues" +Changelog = "https://github.com/didit-protocol/sdk-python/blob/main/CHANGELOG.md" + +[dependency-groups] +dev = [ + "cryptography>=42", + "mypy>=1.13", + "pyright>=1.1.390", + "pytest>=8", + "pytest-cov>=5", + "ruff>=0.8", +] + +[build-system] +requires = ["hatchling>=1.26"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/didit"] + +[tool.hatch.build.targets.sdist] +include = [ + "/src", + "/tests", + "/codegen", + "/openapi", + "/docs", + "/README.md", + "/LICENSE", + "/CHANGELOG.md", +] + +[tool.pytest.ini_options] +testpaths = ["tests", "codegen/tests"] +addopts = "-ra --strict-markers" +markers = [ + "live: talks to the Didit sandbox; runs only when DIDIT_SANDBOX_API_KEY is set", +] +filterwarnings = ["error"] + +[tool.coverage.run] +source = ["didit"] +omit = [ + "*/didit/resources/*", + "*/didit/types/*", +] + +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:", + "@overload", + "raise NotImplementedError", +] + +[tool.mypy] +python_version = "3.10" +strict = true +files = ["src/didit", "tests", "codegen", "examples"] +mypy_path = ["src", "codegen"] +explicit_package_bases = true +warn_unreachable = true +show_error_codes = true + +[[tool.mypy.overrides]] +module = ["fastapi", "fastapi.*", "flask", "flask.*", "django", "django.*", "starlette", "starlette.*", "uvicorn"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +# The framework examples are type-checked without the frameworks installed. +module = ["examples.webhooks.*"] +disallow_untyped_decorators = false +disallow_any_unimported = false +warn_return_any = false + +[tool.pyright] +include = ["src/didit", "tests", "codegen"] +strict = ["src/didit"] +pythonVersion = "3.10" +typeCheckingMode = "standard" +reportMissingTypeStubs = false +reportPrivateUsage = false +reportUnnecessaryIsInstance = false +reportImportCycles = false + +[tool.ruff] +line-length = 100 +target-version = "py310" +src = ["src", "tests", "codegen"] + +[tool.ruff.lint] +select = ["E", "F", "W", "I", "B", "UP", "C4", "SIM", "RUF", "S", "PT", "T20"] +ignore = [ + "E501", + "UP007", + "UP045", + "RUF022", + "RUF036", +] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S101", "S105", "S106", "S108", "S311", "S603"] +"codegen/**" = ["S101", "T20", "S603"] +"examples/**" = ["T20", "S104", "S105"] +"src/didit/resources/**" = ["UP006", "UP035", "RUF001", "RUF002", "RUF003"] +"src/didit/types/**" = ["UP006", "UP035", "RUF001", "RUF002", "RUF003", "E741"] +"tests/generated/**" = ["RUF001", "RUF003", "E741", "SIM101"] + +[tool.ruff.lint.isort] +known-first-party = ["didit", "didit_codegen", "tests", "spec_sync"] diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..7d082e6 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "packages": { + ".": { + "release-type": "python", + "package-name": "didit-sdk", + "changelog-path": "CHANGELOG.md", + "bump-minor-pre-major": true, + "include-component-in-tag": false, + "include-v-in-tag": true, + "extra-files": [ + "src/didit/_version.py", + { + "type": "toml", + "path": "uv.lock", + "jsonpath": "$.package[?(@.name.value=='didit-sdk')].version" + } + ] + } + } +} diff --git a/src/didit/__init__.py b/src/didit/__init__.py new file mode 100644 index 0000000..e49ef8f --- /dev/null +++ b/src/didit/__init__.py @@ -0,0 +1,82 @@ +"""The official Didit Python library. + + from didit import Didit + + client = Didit() # reads DIDIT_API_KEY + session = client.sessions.create(workflow_id="...", vendor_data="user-123") + print(session.url) + +See https://docs.didit.me and the README for the full guide. +""" + +from . import types, webhooks +from ._base_client import ( + DEFAULT_ACCOUNT_BASE_URL, + DEFAULT_BASE_URL, + DEFAULT_MAX_RETRIES, + DEFAULT_TIMEOUT, +) +from ._client import AsyncDidit, Didit +from ._exceptions import ( + APIConnectionError, + APIResponseValidationError, + APIStatusError, + APITimeoutError, + AuthenticationError, + BadRequestError, + ConfigurationError, + ConflictError, + DiditError, + InternalServerError, + NotFoundError, + PermissionDeniedError, + RateLimitError, + UnprocessableEntityError, + WebhookVerificationError, +) +from ._models import BaseModel +from ._pagination import AsyncPage, AsyncPaginator, SyncPage +from ._response import APIResponse, AsyncAPIResponse +from ._types import NOT_GIVEN, FileTypes, NotGiven, Timeout +from ._version import __version__ + +__all__ = [ + "DEFAULT_ACCOUNT_BASE_URL", + "DEFAULT_BASE_URL", + "DEFAULT_MAX_RETRIES", + "DEFAULT_TIMEOUT", + "NOT_GIVEN", + "APIConnectionError", + "APIResponse", + "APIResponseValidationError", + "APIStatusError", + "APITimeoutError", + "AsyncAPIResponse", + "AsyncDidit", + "AsyncPage", + "AsyncPaginator", + "AuthenticationError", + "BadRequestError", + "BaseModel", + "ConfigurationError", + "ConflictError", + "Didit", + "DiditError", + "FileTypes", + "InternalServerError", + "NotFoundError", + "NotGiven", + "PermissionDeniedError", + "RateLimitError", + "SyncPage", + "Timeout", + "UnprocessableEntityError", + "WebhookVerificationError", + "__version__", + "types", + "webhooks", +] + +from ._logging import setup_logging as _setup_logging + +_setup_logging() diff --git a/src/didit/_base_client.py b/src/didit/_base_client.py new file mode 100644 index 0000000..c0ba1d6 --- /dev/null +++ b/src/didit/_base_client.py @@ -0,0 +1,701 @@ +"""HTTP transport shared by :class:`didit.Didit` and :class:`didit.AsyncDidit`. + +Everything that decides *what* is sent and *whether* to retry lives in plain +functions and in :class:`BaseClient`; :class:`SyncAPIClient` and +:class:`AsyncAPIClient` only differ in how they send and sleep. +""" + +from __future__ import annotations + +import asyncio +import email.utils +import logging +import random +import time +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field, replace +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast + +import httpx + +from ._exceptions import ( + APIConnectionError, + APIResponseValidationError, + APITimeoutError, + ConfigurationError, + DiditError, + _request_id, # pyright: ignore[reportPrivateUsage] + make_status_error, +) +from ._files import HttpxFile +from ._models import validate_response +from ._response import APIResponse, AsyncAPIResponse, response_mode +from ._types import NOT_GIVEN, Headers, NotGiven, Query, Timeout +from ._utils import ( + client_header, + encode_json, + encode_path, + multipart_fields, + query_params, + user_agent, +) + +if TYPE_CHECKING: + from ._pagination import AsyncPage, AsyncPaginator, SyncPage + +__all__ = [ + "DEFAULT_ACCOUNT_BASE_URL", + "DEFAULT_BASE_URL", + "DEFAULT_MAX_RETRIES", + "DEFAULT_TIMEOUT", + "AsyncAPIClient", + "BaseClient", + "SyncAPIClient", +] + +log = logging.getLogger("didit") + +DEFAULT_BASE_URL = "https://verification.didit.me" +DEFAULT_ACCOUNT_BASE_URL = "https://apx.didit.me/auth/v2" +DEFAULT_TIMEOUT = 60.0 +DEFAULT_MAX_RETRIES = 2 +INITIAL_RETRY_DELAY = 0.5 +MAX_RETRY_DELAY = 8.0 +MAX_RETRY_AFTER = 60.0 + +AuthKind = Literal["api_key", "account_token", "none"] +Scope = Literal["application"] +Multipart = Literal[False, True, "auto"] + +_IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "PUT", "DELETE", "OPTIONS"}) +# Errors raised before any byte of the request left this process. +_NOT_SENT_ERRORS = (httpx.ConnectError, httpx.ConnectTimeout, httpx.PoolTimeout, httpx.ProxyError) + +T = TypeVar("T") +ItemT = TypeVar("ItemT") + + +@dataclass(frozen=True) +class PreparedRequest: + """A request with everything resolved except the credentials header.""" + + method: str + url: str + auth: AuthKind + origin: str + params: list[tuple[str, str]] = field(default_factory=lambda: []) + json_content: bytes | None = None + data: dict[str, str] | None = None + files: list[tuple[str, HttpxFile]] | None = None + headers: Mapping[str, str | None] = field(default_factory=lambda: {}) + timeout: Timeout | NotGiven = NOT_GIVEN + accept: str = "application/json" + + +def origin_of(url: httpx.URL | str) -> str: + """``scheme://host[:port]`` with the default port left out.""" + parsed = httpx.URL(url) + port = parsed.port + default = {"http": 80, "https": 443}.get(parsed.scheme) + netloc = parsed.host if port is None or port == default else f"{parsed.host}:{port}" + return f"{parsed.scheme}://{netloc}" + + +def should_retry_status(method: str, status: int) -> bool: + """Whether a response with ``status`` may be retried (docs/DESIGN.md section 7). + + ``POST`` and ``PATCH`` reached the server, so only 429 (which guarantees the + request was not processed) is retried for them. + """ + if status == 429: + return True + if method.upper() not in _IDEMPOTENT_METHODS: + return False + return status == 408 or status >= 500 + + +def should_retry_exception(method: str, exc: Exception) -> bool: + """Whether a transport error may be retried. + + A connection that failed before sending is always safe to retry. Anything + later (a read timeout, a dropped connection) may have reached the server, so + it is retried only for idempotent methods. + """ + if isinstance(exc, _NOT_SENT_ERRORS): + return True + if method.upper() not in _IDEMPOTENT_METHODS: + return False + return isinstance(exc, (httpx.TimeoutException, httpx.NetworkError, httpx.RemoteProtocolError)) + + +def parse_retry_after(headers: httpx.Headers, now: float | None = None) -> float | None: + """Seconds to wait from a ``Retry-After`` header (seconds or HTTP date), capped at 60.""" + value = headers.get("retry-after") + if not value: + return None + value = value.strip() + seconds: float | None + try: + seconds = float(value) + except ValueError: + try: + parsed = email.utils.parsedate_to_datetime(value) + except (TypeError, ValueError, IndexError): + return None + seconds = parsed.timestamp() - (time.time() if now is None else now) + if seconds != seconds: # NaN + return None + return max(0.0, min(seconds, MAX_RETRY_AFTER)) + + +def retry_delay(retries_taken: int, headers: httpx.Headers | None) -> float: + """Exponential backoff from 0.5 s with full jitter, capped at 8 s, unless + the server said how long to wait.""" + if headers is not None: + retry_after = parse_retry_after(headers) + if retry_after is not None: + return retry_after + ceiling = min(MAX_RETRY_DELAY, INITIAL_RETRY_DELAY * (2**retries_taken)) + return random.uniform(0, ceiling) # noqa: S311 - jitter, not cryptography + + +def _validate_base_url(name: str, url: str) -> str: + parsed = httpx.URL(url) + if parsed.scheme not in ("http", "https") or not parsed.host: + raise ConfigurationError(f"{name} must be an absolute http(s) URL, got {url!r}.") + return str(parsed).rstrip("/") + + +class BaseClient: + """Configuration and request building shared by the sync and async clients.""" + + api_key: str | None + account_token: str | None + organization_id: str | None + application_id: str | None + base_url: str + account_base_url: str + timeout: Timeout + max_retries: int + default_headers: Mapping[str, str] + + def __init__( + self, + *, + api_key: str | None, + account_token: str | None, + organization_id: str | None, + application_id: str | None, + base_url: str, + account_base_url: str, + timeout: Timeout, + max_retries: int, + default_headers: Mapping[str, str] | None, + ) -> None: + if max_retries < 0: + raise ConfigurationError("max_retries must be 0 or more.") + self.api_key = api_key or None + self.account_token = account_token or None + self.organization_id = organization_id or None + self.application_id = application_id or None + self.base_url = _validate_base_url("base_url", base_url) + self.account_base_url = _validate_base_url("account_base_url", account_base_url) + self.timeout = timeout + self.max_retries = max_retries + self.default_headers = dict(default_headers or {}) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(base_url={self.base_url!r}, " + f"organization_id={self.organization_id!r}, application_id={self.application_id!r}, " + f"api_key={'***' if self.api_key else None})" + ) + + # Request building ------------------------------------------------------- + + def _prepare( + self, + method: str, + path: str, + *, + path_params: Mapping[str, object] | None, + scope: Scope | None, + auth: AuthKind, + api: Literal["verification", "account"], + query: Mapping[str, object] | None, + body: Mapping[str, object] | None, + multipart: Multipart, + file_fields: tuple[str, ...], + headers: Mapping[str, str | NotGiven | None] | None, + extra_headers: Headers | None, + extra_query: Query | None, + extra_body: Mapping[str, object] | None, + timeout: Timeout | NotGiven, + accept: str, + ) -> PreparedRequest: + params = dict(path_params or {}) + if scope == "application": + if not self.organization_id or not self.application_id: + raise ConfigurationError( + "This method is application scoped: pass organization_id and application_id " + "to the client (or set DIDIT_ORGANIZATION_ID and DIDIT_APPLICATION_ID)." + ) + params["organization_id"] = self.organization_id + params["application_id"] = self.application_id + self._check_credentials(auth) + base = self.account_base_url if api == "account" else self.base_url + url = base + encode_path(path, params) + + all_query = query_params(query) + if extra_query: + all_query.extend(query_params(extra_query)) + + json_content: bytes | None = None + data: dict[str, str] | None = None + files: list[tuple[str, HttpxFile]] | None = None + if body is not None or extra_body: + merged: dict[str, object] = dict(body or {}) + if extra_body: + merged.update(extra_body) + use_multipart = multipart is True or ( + multipart == "auto" + and any(merged.get(name) not in (None, NOT_GIVEN) for name in file_fields) + ) + if use_multipart: + data, files = multipart_fields(merged, file_fields) + else: + json_content = encode_json(merged) + + merged_headers: dict[str, str | None] = { + k: v for k, v in (headers or {}).items() if not isinstance(v, NotGiven) + } + if extra_headers: + merged_headers.update(extra_headers) + return PreparedRequest( + method=method.upper(), + url=url, + auth=auth, + origin=origin_of(base), + params=all_query, + json_content=json_content, + data=data, + files=files, + headers=merged_headers, + timeout=timeout, + accept=accept, + ) + + def _check_credentials(self, auth: AuthKind) -> None: + if auth == "api_key" and not self.api_key: + raise ConfigurationError( + "No API key: pass api_key to the client or set the DIDIT_API_KEY environment variable." + ) + if auth == "account_token" and not self.account_token: + raise ConfigurationError( + "This account method needs an account token: call client.account.login(...) and " + "pass its access_token as account_token (client.with_options(account_token=...))." + ) + + def _next_page_request(self, prepared: PreparedRequest, next_url: str) -> PreparedRequest: + """The request for a page's ``next`` link, refused if it leaves the API origin. + + A relative link is resolved against the URL of the page that returned it. + """ + current = httpx.URL(prepared.url, params=prepared.params or None) + try: + absolute = current.join(next_url) + except httpx.InvalidURL as exc: + raise DiditError(f"The API returned an invalid pagination link: {exc}") from exc + if absolute.scheme not in ("http", "https") or origin_of(absolute) != prepared.origin: + raise DiditError( + f"Refusing to follow a pagination link to another origin ({origin_of(absolute)}); " + f"the API key is only sent to {prepared.origin}." + ) + return replace(prepared, url=str(absolute), params=[]) + + def _build_httpx_request( + self, prepared: PreparedRequest, client: httpx.Client | httpx.AsyncClient + ) -> httpx.Request: + # Header names are case-insensitive: a later value replaces an earlier one + # whatever its case, so no header is ever sent twice. + headers: dict[str, str] = {} + removed: set[str] = set() + + def put(name: str, value: str | None) -> None: + for existing in [k for k in headers if k.lower() == name.lower()]: + del headers[existing] + if value is None: + removed.add(name.lower()) + else: + removed.discard(name.lower()) + headers[name] = value + + put("Accept", prepared.accept) + put("User-Agent", user_agent()) + put("X-Didit-Client", client_header()) + for name, default in self.default_headers.items(): + put(name, default) + # Credentials are only ever attached for the origin the request was built for. + if prepared.auth == "api_key" and self.api_key: + put("x-api-key", self.api_key) + elif prepared.auth == "account_token" and self.account_token: + put("Authorization", f"Bearer {self.account_token}") + for name, override in prepared.headers.items(): + put(name, override) + + kwargs: dict[str, Any] = {"headers": headers, "params": prepared.params or None} + if prepared.json_content is not None: + if not any(k.lower() == "content-type" for k in headers): + headers["Content-Type"] = "application/json" + kwargs["content"] = prepared.json_content + elif prepared.files is not None or prepared.data is not None: + if prepared.files: + kwargs["files"] = prepared.files + kwargs["data"] = prepared.data or {} + else: + # httpx sends form fields without files as urlencoded; force multipart. + kwargs["files"] = [(k, (None, v)) for k, v in (prepared.data or {}).items()] + timeout = self.timeout if isinstance(prepared.timeout, NotGiven) else prepared.timeout + kwargs["timeout"] = timeout + request = client.build_request(prepared.method, prepared.url, **kwargs) + # httpx adds its own defaults (User-Agent, ...); honour an explicit removal. + for name in removed: + if name in request.headers: + del request.headers[name] + return request + + # Response handling ------------------------------------------------------ + + def _redactions(self) -> tuple[str, ...]: + return tuple(x for x in (self.api_key, self.account_token) if x) + + def _status_error(self, response: httpx.Response) -> DiditError: + body: object + try: + body = response.json() + except ValueError: + body = response.text or None + return make_status_error(response, body, redact=self._redactions()) + + @staticmethod + def _transport_error(request: httpx.Request, exc: httpx.HTTPError) -> DiditError: + if isinstance(exc, httpx.TimeoutException): + return APITimeoutError(request=request) + return APIConnectionError(request=request) + + @staticmethod + def _parse(response: httpx.Response, cast_to: Any) -> Any: + if cast_to is bytes: + return response.content + if cast_to is None: + return None + if isinstance(cast_to, dict): + by_status = cast("dict[int, Any]", cast_to) + cast_to = by_status.get(response.status_code, next(iter(by_status.values()))) + if response.status_code == 204 or not response.content: + return None + try: + data = response.json() + except ValueError as exc: + raise APIResponseValidationError( + response=response, + message=( + f"Expected a JSON response from {response.request.method} " + f"{response.request.url.path}, got {response.headers.get('content-type', 'no content type')}." + ), + ) from exc + return validate_response(cast_to, data) + + def _log_response( + self, request: httpx.Request, response: httpx.Response, started: float + ) -> None: + if log.isEnabledFor(logging.DEBUG): + log.debug( + "%s %s -> %d in %.0f ms (request id %s)", + request.method, + request.url.path, + response.status_code, + (time.monotonic() - started) * 1000, + _request_id(response.headers) or "-", + ) + + def _log_retry( + self, request: httpx.Request, reason: str, delay: float, retries_left: int + ) -> None: + log.debug( + "%s %s: %s; retrying in %.2f s (%d retries left)", + request.method, + request.url.path, + reason, + delay, + retries_left, + ) + + +class SyncAPIClient(BaseClient): + """The blocking transport, on :class:`httpx.Client`.""" + + _client: httpx.Client + _owns_client: bool + + def _init_http(self, http_client: httpx.Client | None) -> None: + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError( + "http_client must be an httpx.Client (use AsyncDidit for httpx.AsyncClient)." + ) + self._owns_client = http_client is None + self._client = http_client or httpx.Client(follow_redirects=False) + + def close(self) -> None: + """Close the HTTP connection pool (only if this client created it).""" + if self._owns_client: + self._client.close() + + def request( + self, + method: str, + path: str, + *, + cast_to: Any, + path_params: Mapping[str, object] | None = None, + scope: Scope | None = None, + auth: AuthKind = "api_key", + api: Literal["verification", "account"] = "verification", + query: Mapping[str, object] | None = None, + body: Mapping[str, object] | None = None, + multipart: Multipart = False, + file_fields: tuple[str, ...] = (), + headers: Mapping[str, str | NotGiven | None] | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Mapping[str, object] | None = None, + timeout: Timeout | NotGiven = NOT_GIVEN, + accept: str = "application/json", + ) -> Any: + prepared = self._prepare( + method, path, path_params=path_params, scope=scope, auth=auth, api=api, + query=query, body=body, multipart=multipart, file_fields=file_fields, + headers=headers, extra_headers=extra_headers, extra_query=extra_query, + extra_body=extra_body, timeout=timeout, accept=accept, + ) # fmt: skip + return self._send(prepared, lambda r: self._parse(r, cast_to)) + + def request_page( + self, + method: str, + path: str, + *, + cast_to: Any, + item_type: Any, + **kwargs: Any, + ) -> SyncPage[Any]: + from ._pagination import SyncPage + + prepared = self._prepare(method, path, **_prepare_defaults(kwargs)) + return cast( + "SyncPage[Any]", + self._send( + prepared, lambda r: SyncPage.from_response(self, prepared, r, cast_to, item_type) + ), + ) + + def _send(self, prepared: PreparedRequest, parse: Callable[[httpx.Response], T]) -> Any: + mode = response_mode.get() + retries_taken = 0 + while True: + request = self._build_httpx_request(prepared, self._client) + started = time.monotonic() + retry_status = False + try: + response = self._client.send(request, stream=True, follow_redirects=False) + elapsed = time.monotonic() - started + self._log_response(request, response, started) + retry_status = ( + not response.is_success + and retries_taken < self.max_retries + and should_retry_status(request.method, response.status_code) + ) + if retry_status: + response.close() + elif mode != "stream" or not response.is_success: + # Reading the body can fail like sending did (a stalled or + # dropped connection), so it is handled the same way. + try: + response.read() + finally: + response.close() + except httpx.HTTPError as exc: + if retries_taken < self.max_retries and should_retry_exception(request.method, exc): + delay = retry_delay(retries_taken, None) + self._log_retry( + request, _describe(exc), delay, self.max_retries - retries_taken + ) + retries_taken += 1 + time.sleep(delay) + continue + raise self._transport_error(request, exc) from exc + + if retry_status: + delay = retry_delay(retries_taken, response.headers) + self._log_retry( + request, f"HTTP {response.status_code}", delay, self.max_retries - retries_taken + ) + retries_taken += 1 + time.sleep(delay) + continue + if not response.is_success: + raise self._status_error(response) + if mode == "stream" or mode == "raw": + return APIResponse(response, parse, retries_taken, elapsed) + return parse(response) + + +class AsyncAPIClient(BaseClient): + """The asyncio transport, on :class:`httpx.AsyncClient`.""" + + _client: httpx.AsyncClient + _owns_client: bool + + def _init_http(self, http_client: httpx.AsyncClient | None) -> None: + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError( + "http_client must be an httpx.AsyncClient (use Didit for httpx.Client)." + ) + self._owns_client = http_client is None + self._client = http_client or httpx.AsyncClient(follow_redirects=False) + + async def close(self) -> None: + """Close the HTTP connection pool (only if this client created it).""" + if self._owns_client: + await self._client.aclose() + + async def request( + self, + method: str, + path: str, + *, + cast_to: Any, + path_params: Mapping[str, object] | None = None, + scope: Scope | None = None, + auth: AuthKind = "api_key", + api: Literal["verification", "account"] = "verification", + query: Mapping[str, object] | None = None, + body: Mapping[str, object] | None = None, + multipart: Multipart = False, + file_fields: tuple[str, ...] = (), + headers: Mapping[str, str | NotGiven | None] | None = None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Mapping[str, object] | None = None, + timeout: Timeout | NotGiven = NOT_GIVEN, + accept: str = "application/json", + ) -> Any: + prepared = self._prepare( + method, path, path_params=path_params, scope=scope, auth=auth, api=api, + query=query, body=body, multipart=multipart, file_fields=file_fields, + headers=headers, extra_headers=extra_headers, extra_query=extra_query, + extra_body=extra_body, timeout=timeout, accept=accept, + ) # fmt: skip + return await self._send(prepared, lambda r: self._parse(r, cast_to)) + + def request_page( + self, + method: str, + path: str, + *, + cast_to: Any, + item_type: Any, + **kwargs: Any, + ) -> AsyncPaginator[Any]: + from ._pagination import AsyncPage, AsyncPaginator + + prepared = self._prepare(method, path, **_prepare_defaults(kwargs)) + + async def first_page() -> AsyncPage[Any]: + return cast( + "AsyncPage[Any]", + await self._send( + prepared, + lambda r: AsyncPage.from_response(self, prepared, r, cast_to, item_type), + ), + ) + + return AsyncPaginator(first_page) + + async def _send(self, prepared: PreparedRequest, parse: Callable[[httpx.Response], T]) -> Any: + mode = response_mode.get() + retries_taken = 0 + while True: + request = self._build_httpx_request(prepared, self._client) + started = time.monotonic() + retry_status = False + try: + response = await self._client.send(request, stream=True, follow_redirects=False) + elapsed = time.monotonic() - started + self._log_response(request, response, started) + retry_status = ( + not response.is_success + and retries_taken < self.max_retries + and should_retry_status(request.method, response.status_code) + ) + if retry_status: + await response.aclose() + elif mode != "stream" or not response.is_success: + # Reading the body can fail like sending did (a stalled or + # dropped connection), so it is handled the same way. + try: + await response.aread() + finally: + await response.aclose() + except httpx.HTTPError as exc: + if retries_taken < self.max_retries and should_retry_exception(request.method, exc): + delay = retry_delay(retries_taken, None) + self._log_retry( + request, _describe(exc), delay, self.max_retries - retries_taken + ) + retries_taken += 1 + await asyncio.sleep(delay) + continue + raise self._transport_error(request, exc) from exc + + if retry_status: + delay = retry_delay(retries_taken, response.headers) + self._log_retry( + request, f"HTTP {response.status_code}", delay, self.max_retries - retries_taken + ) + retries_taken += 1 + await asyncio.sleep(delay) + continue + if not response.is_success: + raise self._status_error(response) + if mode == "stream" or mode == "raw": + return AsyncAPIResponse(response, parse, retries_taken, elapsed) + return parse(response) + + +def _describe(exc: httpx.HTTPError) -> str: + return "timed out" if isinstance(exc, httpx.TimeoutException) else type(exc).__name__ + + +def _prepare_defaults(kwargs: dict[str, Any]) -> dict[str, Any]: + defaults: dict[str, Any] = { + "path_params": None, + "scope": None, + "auth": "api_key", + "api": "verification", + "query": None, + "body": None, + "multipart": False, + "file_fields": (), + "headers": None, + "extra_headers": None, + "extra_query": None, + "extra_body": None, + "timeout": NOT_GIVEN, + "accept": "application/json", + } + unknown = set(kwargs) - set(defaults) + if unknown: + raise TypeError(f"Unexpected request arguments: {sorted(unknown)}") + defaults.update(kwargs) + return defaults diff --git a/src/didit/_client.py b/src/didit/_client.py new file mode 100644 index 0000000..95cf14d --- /dev/null +++ b/src/didit/_client.py @@ -0,0 +1,222 @@ +"""The :class:`Didit` and :class:`AsyncDidit` clients (docs/DESIGN.md section 6).""" + +from __future__ import annotations + +from collections.abc import Mapping +from types import TracebackType + +import httpx +from typing_extensions import Self + +from ._base_client import ( + DEFAULT_ACCOUNT_BASE_URL, + DEFAULT_BASE_URL, + DEFAULT_MAX_RETRIES, + DEFAULT_TIMEOUT, +) +from ._types import NOT_GIVEN, NotGiven, Timeout +from ._utils import env +from .resources import AsyncRootResources, SyncRootResources + +__all__ = ["AsyncDidit", "Didit"] + + +def _pick(value: str | None | NotGiven, current: str | None) -> str | None: + return current if isinstance(value, NotGiven) else value + + +class Didit(SyncRootResources): + """The Didit API client. + + ``client = Didit()`` reads the API key from ``DIDIT_API_KEY`` and the + organization and application ids from ``DIDIT_ORGANIZATION_ID`` and + ``DIDIT_APPLICATION_ID``. Create one client per process and reuse it: it + keeps a connection pool and is safe to share between threads. + """ + + def __init__( + self, + api_key: str | None = None, + *, + base_url: str | None = None, + organization_id: str | None = None, + application_id: str | None = None, + account_base_url: str | None = None, + account_token: str | None = None, + timeout: Timeout = DEFAULT_TIMEOUT, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + http_client: httpx.Client | None = None, + ) -> None: + """Create a client. + + Args: + api_key: The application's API key (header ``x-api-key``). Defaults to + ``DIDIT_API_KEY``. Only needed for the methods that use it, so a client + without one can still call ``client.account`` and verify webhooks. + base_url: Defaults to ``https://verification.didit.me``. + organization_id: Needed by application-scoped methods (cases, networks, + regulatory reports...). Defaults to ``DIDIT_ORGANIZATION_ID``. + application_id: Needed by application-scoped methods. Defaults to + ``DIDIT_APPLICATION_ID``. + account_base_url: Base URL of the account management API. Defaults to + ``https://apx.didit.me/auth/v2``. + account_token: The ``access_token`` from ``client.account.login(...)``, + for the ``client.account`` methods that need it. + timeout: Seconds, or an :class:`httpx.Timeout`. Default 60. + max_retries: Retries of failed requests that are safe to retry. Default 2. + default_headers: Headers to add to every request. + http_client: Your own :class:`httpx.Client` (proxies, custom transports, + mocks). The SDK does not close a client you pass in. + """ + super().__init__( + api_key=api_key if api_key is not None else env("DIDIT_API_KEY"), + account_token=account_token, + organization_id=organization_id + if organization_id is not None + else env("DIDIT_ORGANIZATION_ID"), + application_id=application_id + if application_id is not None + else env("DIDIT_APPLICATION_ID"), + base_url=base_url or DEFAULT_BASE_URL, + account_base_url=account_base_url or DEFAULT_ACCOUNT_BASE_URL, + timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + ) + self._init_http(http_client) + + def with_options( + self, + *, + api_key: str | None | NotGiven = NOT_GIVEN, + base_url: str | NotGiven = NOT_GIVEN, + organization_id: str | None | NotGiven = NOT_GIVEN, + application_id: str | None | NotGiven = NOT_GIVEN, + account_base_url: str | NotGiven = NOT_GIVEN, + account_token: str | None | NotGiven = NOT_GIVEN, + timeout: Timeout | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | NotGiven = NOT_GIVEN, + ) -> Self: + """A copy of this client with some options changed. It shares the + connection pool, so it is cheap: ``client.with_options(max_retries=5).sessions.list()``.""" + copy = type(self).__new__(type(self)) + SyncRootResources.__init__( + copy, + api_key=_pick(api_key, self.api_key), + account_token=_pick(account_token, self.account_token), + organization_id=_pick(organization_id, self.organization_id), + application_id=_pick(application_id, self.application_id), + base_url=self.base_url if isinstance(base_url, NotGiven) else base_url, + account_base_url=self.account_base_url + if isinstance(account_base_url, NotGiven) + else account_base_url, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + max_retries=self.max_retries if isinstance(max_retries, NotGiven) else max_retries, + default_headers=self.default_headers + if isinstance(default_headers, NotGiven) + else default_headers, + ) + copy._client = self._client + copy._owns_client = False + return copy + + copy = with_options + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close() + + +class AsyncDidit(AsyncRootResources): + """The asyncio Didit API client. Same options as :class:`Didit`; + every method is awaitable and lists support ``async for``.""" + + def __init__( + self, + api_key: str | None = None, + *, + base_url: str | None = None, + organization_id: str | None = None, + application_id: str | None = None, + account_base_url: str | None = None, + account_token: str | None = None, + timeout: Timeout = DEFAULT_TIMEOUT, + max_retries: int = DEFAULT_MAX_RETRIES, + default_headers: Mapping[str, str] | None = None, + http_client: httpx.AsyncClient | None = None, + ) -> None: + """Create a client. See :class:`Didit` for the options; ``http_client`` + is an :class:`httpx.AsyncClient` here.""" + super().__init__( + api_key=api_key if api_key is not None else env("DIDIT_API_KEY"), + account_token=account_token, + organization_id=organization_id + if organization_id is not None + else env("DIDIT_ORGANIZATION_ID"), + application_id=application_id + if application_id is not None + else env("DIDIT_APPLICATION_ID"), + base_url=base_url or DEFAULT_BASE_URL, + account_base_url=account_base_url or DEFAULT_ACCOUNT_BASE_URL, + timeout=timeout, + max_retries=max_retries, + default_headers=default_headers, + ) + self._init_http(http_client) + + def with_options( + self, + *, + api_key: str | None | NotGiven = NOT_GIVEN, + base_url: str | NotGiven = NOT_GIVEN, + organization_id: str | None | NotGiven = NOT_GIVEN, + application_id: str | None | NotGiven = NOT_GIVEN, + account_base_url: str | NotGiven = NOT_GIVEN, + account_token: str | None | NotGiven = NOT_GIVEN, + timeout: Timeout | NotGiven = NOT_GIVEN, + max_retries: int | NotGiven = NOT_GIVEN, + default_headers: Mapping[str, str] | NotGiven = NOT_GIVEN, + ) -> Self: + """A copy of this client with some options changed, sharing the connection pool.""" + copy = type(self).__new__(type(self)) + AsyncRootResources.__init__( + copy, + api_key=_pick(api_key, self.api_key), + account_token=_pick(account_token, self.account_token), + organization_id=_pick(organization_id, self.organization_id), + application_id=_pick(application_id, self.application_id), + base_url=self.base_url if isinstance(base_url, NotGiven) else base_url, + account_base_url=self.account_base_url + if isinstance(account_base_url, NotGiven) + else account_base_url, + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + max_retries=self.max_retries if isinstance(max_retries, NotGiven) else max_retries, + default_headers=self.default_headers + if isinstance(default_headers, NotGiven) + else default_headers, + ) + copy._client = self._client + copy._owns_client = False + return copy + + copy = with_options + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self.close() diff --git a/src/didit/_exceptions.py b/src/didit/_exceptions.py new file mode 100644 index 0000000..db5904a --- /dev/null +++ b/src/didit/_exceptions.py @@ -0,0 +1,252 @@ +"""The exceptions raised by the SDK (docs/DESIGN.md section 7).""" + +from __future__ import annotations + +from typing import Any, Literal + +import httpx +from typing_extensions import override + +__all__ = [ + "APIConnectionError", + "APIResponseValidationError", + "APIStatusError", + "APITimeoutError", + "AuthenticationError", + "BadRequestError", + "ConfigurationError", + "ConflictError", + "DiditError", + "InternalServerError", + "NotFoundError", + "PermissionDeniedError", + "RateLimitError", + "UnprocessableEntityError", + "WebhookVerificationError", + "WebhookVerificationReason", +] + + +# Request headers that carry credentials. Every error that keeps a request blanks +# them, so error reporters that serialise exception attributes never capture them. +_CREDENTIAL_HEADERS = ("x-api-key", "authorization") + + +def _scrub(request: httpx.Request) -> httpx.Request: + for name in _CREDENTIAL_HEADERS: + if name in request.headers: + request.headers[name] = "[redacted]" + return request + + +def _scrub_response(response: httpx.Response) -> None: + try: + request = response.request + except RuntimeError: # a response built without a request + return + _scrub(request) + + +class DiditError(Exception): + """Base class of every error the SDK raises.""" + + message: str + + def __init__(self, message: str) -> None: + super().__init__(message) + self.message = message + + +class ConfigurationError(DiditError): + """The client is missing something it needs: an API key, an account token, + the organization or application id, or a webhook secret. Raised before any + request is sent.""" + + +class APIConnectionError(DiditError): + """No response arrived: the connection failed or broke.""" + + request: httpx.Request + + def __init__(self, *, request: httpx.Request, message: str = "Connection error.") -> None: + super().__init__(message) + self.request = _scrub(request) + + +class APITimeoutError(APIConnectionError): + """The request timed out before a response arrived.""" + + def __init__(self, *, request: httpx.Request) -> None: + super().__init__(request=request, message="Request timed out.") + + +class APIResponseValidationError(DiditError): + """A successful response could not be read as the documented content (for + example HTML where JSON was expected).""" + + response: httpx.Response + status: int + + def __init__(self, *, response: httpx.Response, message: str) -> None: + super().__init__(message) + _scrub_response(response) + self.response = response + self.status = response.status_code + + +class APIStatusError(DiditError): + """The API answered with a non-2xx status.""" + + status: int + """The HTTP status code.""" + body: object + """The parsed JSON body when the response was JSON, else the text, else ``None``.""" + headers: httpx.Headers + request_id: str | None + response: httpx.Response + + def __init__(self, message: str, *, response: httpx.Response, body: object) -> None: + super().__init__(message) + _scrub_response(response) + self.response = response + self.status = response.status_code + self.headers = response.headers + self.body = body + self.request_id = _request_id(response.headers) + + @property + def status_code(self) -> int: + """Alias of :attr:`status`.""" + return self.status + + @override + def __str__(self) -> str: + suffix = f" (request id {self.request_id})" if self.request_id else "" + return f"{self.message}{suffix}" + + +class BadRequestError(APIStatusError): + """HTTP 400.""" + + +class AuthenticationError(APIStatusError): + """HTTP 401.""" + + +class PermissionDeniedError(APIStatusError): + """HTTP 403.""" + + +class NotFoundError(APIStatusError): + """HTTP 404.""" + + +class ConflictError(APIStatusError): + """HTTP 409.""" + + +class UnprocessableEntityError(APIStatusError): + """HTTP 422.""" + + +class RateLimitError(APIStatusError): + """HTTP 429.""" + + +class InternalServerError(APIStatusError): + """HTTP 5xx.""" + + +WebhookVerificationReason = Literal[ + "missing_signature", + "invalid_signature", + "missing_timestamp", + "timestamp_mismatch", + "timestamp_out_of_tolerance", + "invalid_payload", +] + + +class WebhookVerificationError(DiditError): + """A webhook delivery failed verification (docs/DESIGN.md section 9.3). + + ``reason`` is machine readable: ``missing_signature``, ``invalid_signature``, + ``missing_timestamp``, ``timestamp_mismatch``, ``timestamp_out_of_tolerance`` + or ``invalid_payload``. + """ + + reason: WebhookVerificationReason + + def __init__(self, reason: WebhookVerificationReason, message: str) -> None: + super().__init__(message) + self.reason = reason + + +_STATUS_ERRORS: dict[int, type[APIStatusError]] = { + 400: BadRequestError, + 401: AuthenticationError, + 403: PermissionDeniedError, + 404: NotFoundError, + 409: ConflictError, + 422: UnprocessableEntityError, + 429: RateLimitError, +} + +_REQUEST_ID_HEADERS = ("x-request-id", "request-id", "x-amzn-requestid", "x-correlation-id") + + +def _request_id(headers: httpx.Headers) -> str | None: + for name in _REQUEST_ID_HEADERS: + value: str | None = headers.get(name) + if value: + return value + return None + + +def _error_message(status: int, body: object) -> str: + detail: Any = None + if isinstance(body, dict): + error, description = body.get("error"), body.get("error_description") # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] + if isinstance(error, str) and error and isinstance(description, str) and description: + # OAuth 2.0 errors (RFC 6749 section 5.2): the code and what it means. + body = {"detail": f"{error}: {description}", **body} # pyright: ignore[reportUnknownVariableType] + for key in ("detail", "message", "error", "error_description"): + value = body.get(key) # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] + if isinstance(value, str) and value: + detail = value + break + if detail is None and body: + detail = "; ".join(_field_errors(body)) # pyright: ignore[reportUnknownArgumentType] + elif isinstance(body, list) and body and all(isinstance(x, str) for x in body): # pyright: ignore[reportUnknownVariableType] + detail = "; ".join(body) # pyright: ignore[reportUnknownArgumentType] + elif isinstance(body, str) and body.strip() and len(body) <= 500: + detail = body.strip() + text = f"HTTP {status}" + if detail: + text += f": {detail}" + return text + + +def _field_errors(body: dict[str, Any]) -> list[str]: + parts: list[str] = [] + for key, value in list(body.items())[:5]: + if isinstance(value, list) and value and isinstance(value[0], str): + parts.append(f"{key}: {value[0]}") + elif isinstance(value, str): + parts.append(f"{key}: {value}") + return parts + + +def make_status_error( + response: httpx.Response, body: object, *, redact: tuple[str, ...] = () +) -> APIStatusError: + """Build the right :class:`APIStatusError` subclass for ``response``.""" + status = response.status_code + message = _error_message(status, body) + for secret in redact: + if secret and secret in message: + message = message.replace(secret, "[redacted]") + cls = _STATUS_ERRORS.get(status) + if cls is None: + cls = InternalServerError if status >= 500 else APIStatusError + return cls(message, response=response, body=body) diff --git a/src/didit/_files.py b/src/didit/_files.py new file mode 100644 index 0000000..ab55403 --- /dev/null +++ b/src/didit/_files.py @@ -0,0 +1,95 @@ +"""File inputs for multipart uploads (docs/DESIGN.md section 8).""" + +from __future__ import annotations + +import mimetypes +import os +import pathlib +from typing import Any, Union, cast + +from ._types import FileContent, FileTypes + +__all__ = ["guess_content_type", "to_httpx_file"] + +HttpxFile = tuple[Union[str, None], bytes, Union[str, None]] + +# The upload types the API accepts. ``mimetypes`` alone depends on the host (the +# Windows registry, /etc/mime.types), so these never vary between machines. +_CONTENT_TYPES = { + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".png": "image/png", + ".webp": "image/webp", + ".tif": "image/tiff", + ".tiff": "image/tiff", + ".heic": "image/heic", + ".heif": "image/heif", + ".gif": "image/gif", + ".bmp": "image/bmp", + ".pdf": "application/pdf", + ".zip": "application/zip", + ".csv": "text/csv", + ".ndjson": "application/x-ndjson", + ".json": "application/json", + ".mp4": "video/mp4", + ".webm": "video/webm", + ".mov": "video/quicktime", +} + + +def guess_content_type(filename: str) -> str: + """The content type for ``filename``, the same on every operating system for + the types the API accepts.""" + known = _CONTENT_TYPES.get(os.path.splitext(filename)[1].lower()) + if known is not None: + return known + return mimetypes.guess_type(filename)[0] or "application/octet-stream" + + +def to_httpx_file(value: FileTypes) -> HttpxFile: + """Read a file input into ``(filename, content, content_type)``. + + The content is read once into memory so that a retried request sends the + same bytes again. + """ + filename: str | None = None + content_type: str | None = None + content: FileContent + if isinstance(value, tuple): + parts = cast("tuple[Any, ...]", value) + if len(parts) == 2: + filename, content = parts + elif len(parts) == 3: + filename, content, content_type = parts + else: + raise TypeError( + "A file tuple is (filename, content) or (filename, content, content_type)." + ) + else: + content = value + data: bytes + if isinstance(content, (bytes, bytearray)): + data = bytes(content) + elif isinstance(content, os.PathLike): + path = pathlib.Path(content) + data = path.read_bytes() + if filename is None: + filename = path.name + elif hasattr(content, "read"): + raw: object = content.read() + if isinstance(raw, str): + raise TypeError("Open files in binary mode ('rb') before uploading them.") + data = bytes(cast("bytes | bytearray", raw)) + if filename is None: + name = getattr(content, "name", None) + if isinstance(name, str): + filename = os.path.basename(name) + else: + raise TypeError( + f"Expected a path, bytes or a binary stream for a file, got {type(content).__name__}." + ) + if filename is None: + filename = "upload" + if content_type is None: + content_type = guess_content_type(filename) + return filename, data, content_type diff --git a/src/didit/_logging.py b/src/didit/_logging.py new file mode 100644 index 0000000..533eca2 --- /dev/null +++ b/src/didit/_logging.py @@ -0,0 +1,27 @@ +"""Debug logging (docs/DESIGN.md section 6). + +The SDK logs to the ``didit`` logger at debug level: method, path, status, +duration and request id, never bodies or headers. ``DIDIT_LOG=debug`` also +prints those lines to stderr without any logging setup in your app. +""" + +from __future__ import annotations + +import logging +import os + +__all__ = ["setup_logging"] + +logger = logging.getLogger("didit") + + +def setup_logging() -> None: + level = (os.environ.get("DIDIT_LOG") or "").strip().lower() + if level != "debug": + return + if not any(getattr(h, "_didit", False) for h in logger.handlers): + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("[%(asctime)s %(name)s %(levelname)s] %(message)s")) + handler._didit = True # type: ignore[attr-defined] + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) diff --git a/src/didit/_models.py b/src/didit/_models.py new file mode 100644 index 0000000..7772628 --- /dev/null +++ b/src/didit/_models.py @@ -0,0 +1,159 @@ +"""Response model base class and tolerant response parsing. + +Response models keep every field the API sends, including fields this version +of the SDK does not know yet (``model_extra``). A response that does not match +the published contract is still returned: when strict validation fails, the +model is built field by field without raising (docs/DESIGN.md section 5.1). +""" + +from __future__ import annotations + +import datetime as _dt +import functools +import logging +import types +from typing import Annotated, Any, Literal, Union, cast, get_args, get_origin + +import pydantic +import typing_extensions +from typing_extensions import Self + +__all__ = ["BaseModel", "construct_type", "validate_response"] + +log = logging.getLogger("didit") + + +class BaseModel(pydantic.BaseModel): + """Base class of every response model. + + Unknown fields are kept and readable through ``model_extra`` (or as + attributes). ``model_fields_set`` tells a field that was absent from one + that was sent as ``null``. + """ + + model_config = pydantic.ConfigDict( + extra="allow", + defer_build=True, + protected_namespaces=(), + ) + + def to_dict(self) -> dict[str, Any]: + """The model as plain JSON data, with the API's field names, without + fields the API did not send.""" + # warnings=False: a response that did not match the schema keeps the + # values the API sent, which pydantic would otherwise warn about. + return self.model_dump(mode="json", by_alias=True, exclude_unset=True, warnings=False) + + def to_json(self, *, indent: int | None = 2) -> str: + """The model as a JSON string, with the API's field names.""" + return self.model_dump_json( + by_alias=True, exclude_unset=True, indent=indent, warnings=False + ) + + @classmethod + def construct_lenient(cls, data: dict[str, Any]) -> Self: + """Build the model from ``data`` without raising on contract mismatches. + + A required field the API left out reads ``None`` (instead of raising + ``AttributeError`` on access) and stays out of ``model_fields_set``. + """ + by_alias = {(field.alias or name): name for name, field in cls.model_fields.items()} + values: dict[str, Any] = {} + sent: set[str] = set() + for key, value in data.items(): + name = by_alias.get(key) + if name is None: + values[key] = value + continue + sent.add(name) + values[key] = construct_type(cls.model_fields[name].annotation, value) + for name, field in cls.model_fields.items(): + if name not in sent and field.is_required(): + values[field.alias or name] = None + return cls.model_construct(_fields_set=sent, **values) + + +@functools.cache +def _adapter(type_: Any) -> pydantic.TypeAdapter[Any]: + return pydantic.TypeAdapter(type_) + + +def _is_union(origin: Any) -> bool: + return origin is Union or origin is types.UnionType + + +def construct_type(type_: Any, value: object) -> object: + """Best-effort conversion of ``value`` to ``type_`` that never raises.""" + if value is None or type_ is Any or type_ is object: + return value + origin = get_origin(type_) + if origin is Annotated: + return construct_type(get_args(type_)[0], value) + if _is_union(origin): + members = [arg for arg in get_args(type_) if arg is not type(None)] + for member in members: + try: + return _adapter(member).validate_python(value) + except pydantic.ValidationError: + continue + if len(members) == 1: + return construct_type(members[0], value) + if isinstance(value, list): + lists = [m for m in members if get_origin(m) is list] + if lists: + return construct_type(lists[0], cast("list[object]", value)) + if isinstance(value, dict): + models = [m for m in members if isinstance(m, type) and issubclass(m, BaseModel)] + if models: + best = max(models, key=lambda m: _overlap(m, cast("dict[str, Any]", value))) + return best.construct_lenient(cast("dict[str, Any]", value)) + return value # pyright: ignore[reportUnknownVariableType] + if isinstance(type_, type) and issubclass(type_, BaseModel): + if isinstance(value, dict): + try: + return type_.model_validate(value) + except pydantic.ValidationError: + return type_.construct_lenient(cast("dict[str, Any]", value)) + return value + if origin in (list, list): + if isinstance(value, list): + (inner,) = get_args(type_) or (Any,) + return [construct_type(inner, item) for item in cast("list[object]", value)] + return value + if origin in (dict, dict): + if isinstance(value, dict): + args = get_args(type_) + inner = args[1] if len(args) == 2 else Any + return { + k: construct_type(inner, v) for k, v in cast("dict[str, object]", value).items() + } + return value + if origin is Literal or origin is typing_extensions.Literal: + return value + if type_ in (_dt.datetime, _dt.date): + try: + return _adapter(type_).validate_python(value) + except pydantic.ValidationError: + return value + return value + + +def _overlap(model: type[BaseModel], data: dict[str, Any]) -> int: + names = {(field.alias or name) for name, field in model.model_fields.items()} + return len(names.intersection(data)) + + +def validate_response(type_: Any, data: object) -> Any: + """Validate ``data`` as ``type_``; fall back to :func:`construct_type`.""" + if type_ is Any or type_ is object: + return data + try: + return _adapter(type_).validate_python(data) + except pydantic.ValidationError as exc: + log.debug( + "Response does not match the published schema of %s (%d errors); " + "returning it without validation.", + getattr(type_, "__name__", type_), + exc.error_count(), + ) + return construct_type(type_, data) diff --git a/src/didit/_pagination.py b/src/didit/_pagination.py new file mode 100644 index 0000000..e1cec74 --- /dev/null +++ b/src/didit/_pagination.py @@ -0,0 +1,240 @@ +"""Pages of ``limit``/``offset`` lists with auto-pagination (docs/DESIGN.md section 8). + +``for item in client.sessions.list()`` walks every item of every page; +``async for item in client.sessions.list()`` does the same on :class:`AsyncDidit`. +""" + +from __future__ import annotations + +from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator +from typing import ( + TYPE_CHECKING, + Any, + Generic, + TypeVar, + cast, +) + +import httpx + +from ._exceptions import APIResponseValidationError, DiditError +from ._models import BaseModel, validate_response +from ._response import response_mode + +if TYPE_CHECKING: + from ._base_client import AsyncAPIClient, PreparedRequest, SyncAPIClient + +__all__ = ["AsyncPage", "AsyncPaginator", "SyncPage"] + +ItemT = TypeVar("ItemT") + + +class _BasePage(Generic[ItemT]): + count: int | None + """Total number of items across all pages, when the API reports it.""" + next: str | None + """URL of the next page, or ``None`` on the last page.""" + previous: str | None + """URL of the previous page, or ``None`` on the first page.""" + results: list[ItemT] + """The items on this page.""" + response: BaseModel | None + """The whole response body as its generated model, including any fields besides + ``count``, ``next``, ``previous`` and ``results``.""" + + def __init__( + self, + *, + results: list[ItemT], + count: int | None, + next: str | None, + previous: str | None, + response: BaseModel | None, + prepared: PreparedRequest, + cast_to: Any, + item_type: Any, + ) -> None: + self.results = results + self.count = count + self.next = next + self.previous = previous + self.response = response + self._prepared = prepared + self._cast_to = cast_to + self._item_type = item_type + + @staticmethod + def _fields(response: httpx.Response, cast_to: Any, item_type: Any) -> dict[str, Any]: + try: + data: object = response.json() if response.content else None + except ValueError: + data = None + if not isinstance(data, dict): + raise APIResponseValidationError( + response=response, + message=( + f"Expected a paginated JSON object with a 'results' list from " + f"{response.request.method} {response.request.url.path}, got " + f"{response.headers.get('content-type', 'no content type')}." + ), + ) + body = cast("dict[str, Any]", data) + parsed = validate_response(cast_to, body) + raw_results = body.get("results") + results = cast("list[Any]", validate_response(list[item_type], raw_results or [])) + count = body.get("count") + return { + "results": results, + "count": count if isinstance(count, int) else None, + "next": body.get("next") if isinstance(body.get("next"), str) else None, + "previous": body.get("previous") if isinstance(body.get("previous"), str) else None, + "response": parsed if isinstance(parsed, BaseModel) else None, + } + + def has_next_page(self) -> bool: + return bool(self.next) and bool(self.results) + + def _check_not_seen(self, seen: set[str]) -> None: + # A server that keeps returning a link it already returned would make + # auto-pagination loop forever. + if self.next in seen: + raise DiditError( + f"The API returned the pagination link {self.next!r} twice; " + "stopping instead of looping forever." + ) + seen.add(cast("str", self.next)) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} count={self.count} results={len(self.results)} " + f"next={self.next!r}>" + ) + + +class SyncPage(_BasePage[ItemT]): + """One page of a list. Iterating it yields every item of every page.""" + + _client: SyncAPIClient + + @classmethod + def from_response( + cls, + client: SyncAPIClient, + prepared: PreparedRequest, + response: httpx.Response, + cast_to: Any, + item_type: Any, + ) -> SyncPage[Any]: + page: SyncPage[Any] = cls( + **cls._fields(response, cast_to, item_type), + prepared=prepared, + cast_to=cast_to, + item_type=item_type, + ) + page._client = client + return page + + def get_next_page(self) -> SyncPage[ItemT]: + """Fetch the next page. The link must point at the API's own origin.""" + if not self.next: + raise DiditError("This is the last page.") + prepared = self._client._next_page_request(self._prepared, self.next) # pyright: ignore[reportPrivateUsage] + cast_to, item_type, client = self._cast_to, self._item_type, self._client + token = response_mode.set("parsed") + try: + return cast( + "SyncPage[ItemT]", + client._send( # pyright: ignore[reportPrivateUsage] + prepared, + lambda r: SyncPage.from_response(client, prepared, r, cast_to, item_type), + ), + ) + finally: + response_mode.reset(token) + + def iter_pages(self) -> Iterator[SyncPage[ItemT]]: + page: SyncPage[ItemT] = self + seen: set[str] = set() + while True: + yield page + if not page.has_next_page(): + return + page._check_not_seen(seen) + page = page.get_next_page() + + def __iter__(self) -> Iterator[ItemT]: + for page in self.iter_pages(): + yield from page.results + + +class AsyncPage(_BasePage[ItemT]): + """One page of a list. ``async for`` over it yields every item of every page.""" + + _client: AsyncAPIClient + + @classmethod + def from_response( + cls, + client: AsyncAPIClient, + prepared: PreparedRequest, + response: httpx.Response, + cast_to: Any, + item_type: Any, + ) -> AsyncPage[Any]: + page: AsyncPage[Any] = cls( + **cls._fields(response, cast_to, item_type), + prepared=prepared, + cast_to=cast_to, + item_type=item_type, + ) + page._client = client + return page + + async def get_next_page(self) -> AsyncPage[ItemT]: + """Fetch the next page. The link must point at the API's own origin.""" + if not self.next: + raise DiditError("This is the last page.") + prepared = self._client._next_page_request(self._prepared, self.next) # pyright: ignore[reportPrivateUsage] + cast_to, item_type, client = self._cast_to, self._item_type, self._client + token = response_mode.set("parsed") + try: + return cast( + "AsyncPage[ItemT]", + await client._send( # pyright: ignore[reportPrivateUsage] + prepared, + lambda r: AsyncPage.from_response(client, prepared, r, cast_to, item_type), + ), + ) + finally: + response_mode.reset(token) + + async def iter_pages(self) -> AsyncIterator[AsyncPage[ItemT]]: + page: AsyncPage[ItemT] = self + seen: set[str] = set() + while True: + yield page + if not page.has_next_page(): + return + page._check_not_seen(seen) + page = await page.get_next_page() + + async def __aiter__(self) -> AsyncIterator[ItemT]: + async for page in self.iter_pages(): + for item in page.results: + yield item + + +class AsyncPaginator(Generic[ItemT]): + """What an async list method returns: ``await`` it for the first page, or + ``async for`` over it for every item.""" + + def __init__(self, first_page: Callable[[], Awaitable[AsyncPage[ItemT]]]) -> None: + self._first_page = first_page + + def __await__(self) -> Generator[Any, None, AsyncPage[ItemT]]: + return self._first_page().__await__() + + async def __aiter__(self) -> AsyncIterator[ItemT]: + page = await self._first_page() + async for item in page: + yield item diff --git a/src/didit/_resource.py b/src/didit/_resource.py new file mode 100644 index 0000000..9cd9b5e --- /dev/null +++ b/src/didit/_resource.py @@ -0,0 +1,24 @@ +"""Base classes of the generated resources.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._base_client import AsyncAPIClient, SyncAPIClient + +__all__ = ["AsyncAPIResource", "SyncAPIResource"] + + +class SyncAPIResource: + _client: SyncAPIClient + + def __init__(self, client: SyncAPIClient) -> None: + self._client = client + + +class AsyncAPIResource: + _client: AsyncAPIClient + + def __init__(self, client: AsyncAPIClient) -> None: + self._client = client diff --git a/src/didit/_response.py b/src/didit/_response.py new file mode 100644 index 0000000..4231483 --- /dev/null +++ b/src/didit/_response.py @@ -0,0 +1,299 @@ +"""Raw and streamed HTTP response access (docs/DESIGN.md section 7). + +``client.sessions.with_raw_response.create(...)`` returns an :class:`APIResponse` +with the status, the headers and the request id; ``.parse()`` gives the value the +plain method returns. ``with_streaming_response`` does the same without reading +the body first, for large binary downloads. +""" + +from __future__ import annotations + +import contextlib +import contextvars +import functools +from collections.abc import AsyncIterator, Awaitable, Callable, Generator, Iterator +from types import TracebackType +from typing import ( + Generic, + Literal, + TypeVar, + cast, +) + +import httpx +from typing_extensions import ParamSpec + +from ._exceptions import ( + APIConnectionError, + APITimeoutError, + _request_id, # pyright: ignore[reportPrivateUsage] +) + +__all__ = [ + "APIResponse", + "AsyncAPIResponse", + "AsyncResponseContextManager", + "ResponseContextManager", + "async_to_raw_response_wrapper", + "async_to_streamed_response_wrapper", + "to_raw_response_wrapper", + "to_streamed_response_wrapper", +] + +R = TypeVar("R") +P = ParamSpec("P") + +ResponseMode = Literal["parsed", "raw", "stream"] +response_mode: contextvars.ContextVar[ResponseMode] = contextvars.ContextVar( + "didit_response_mode", default="parsed" +) + + +@contextlib.contextmanager +def _reading(response: httpx.Response) -> Generator[None, None, None]: + """Turn an error while reading a streamed body into the SDK's own errors.""" + try: + yield + except httpx.TimeoutException as exc: + raise APITimeoutError(request=response.request) from exc + except httpx.HTTPError as exc: + raise APIConnectionError(request=response.request) from exc + + +class _BaseAPIResponse(Generic[R]): + http_response: httpx.Response + retries_taken: int + + def __init__( + self, + http_response: httpx.Response, + parse: Callable[[httpx.Response], R], + retries_taken: int, + elapsed: float = 0.0, + ) -> None: + self.http_response = http_response + self.retries_taken = retries_taken + self._elapsed = elapsed + self._parse = parse + self._parsed: list[R] = [] + + @property + def status_code(self) -> int: + return self.http_response.status_code + + @property + def headers(self) -> httpx.Headers: + return self.http_response.headers + + @property + def request_id(self) -> str | None: + return _request_id(self.http_response.headers) + + @property + def url(self) -> httpx.URL: + return self.http_response.url + + @property + def method(self) -> str: + return self.http_response.request.method + + @property + def elapsed(self) -> float: + """Seconds between sending the last attempt and receiving its response headers.""" + return self._elapsed + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} [{self.status_code}] {self.method} {self.url.path}" + f" request_id={self.request_id!r}>" + ) + + +class APIResponse(_BaseAPIResponse[R]): + """A raw HTTP response; :meth:`parse` returns the typed value.""" + + def parse(self) -> R: + if not self._parsed: + self.read() + self._parsed.append(self._parse(self.http_response)) + return self._parsed[0] + + def read(self) -> bytes: + with _reading(self.http_response): + return self.http_response.read() + + def text(self) -> str: + self.read() + return self.http_response.text + + def json(self) -> object: + self.read() + return self.http_response.json() + + def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]: + with _reading(self.http_response): + yield from self.http_response.iter_bytes(chunk_size) + + def iter_text(self, chunk_size: int | None = None) -> Iterator[str]: + with _reading(self.http_response): + yield from self.http_response.iter_text(chunk_size) + + def iter_lines(self) -> Iterator[str]: + with _reading(self.http_response): + yield from self.http_response.iter_lines() + + def write_to_file(self, path: str) -> None: + """Stream the body to ``path``.""" + with open(path, "wb") as fh: + for chunk in self.iter_bytes(): + fh.write(chunk) + + def close(self) -> None: + self.http_response.close() + + +class AsyncAPIResponse(_BaseAPIResponse[R]): + """A raw HTTP response; ``await`` :meth:`parse` for the typed value.""" + + async def parse(self) -> R: + if not self._parsed: + await self.read() + self._parsed.append(self._parse(self.http_response)) + return self._parsed[0] + + async def read(self) -> bytes: + with _reading(self.http_response): + return await self.http_response.aread() + + async def text(self) -> str: + await self.read() + return self.http_response.text + + async def json(self) -> object: + await self.read() + return self.http_response.json() + + async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]: + with _reading(self.http_response): + async for chunk in self.http_response.aiter_bytes(chunk_size): + yield chunk + + async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]: + with _reading(self.http_response): + async for chunk in self.http_response.aiter_text(chunk_size): + yield chunk + + async def iter_lines(self) -> AsyncIterator[str]: + with _reading(self.http_response): + async for line in self.http_response.aiter_lines(): + yield line + + async def write_to_file(self, path: str) -> None: + """Stream the body to ``path``.""" + with open(path, "wb") as fh: + async for chunk in self.iter_bytes(): + fh.write(chunk) + + async def close(self) -> None: + await self.http_response.aclose() + + +def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]: + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]: + token = response_mode.set("raw") + try: + return cast("APIResponse[R]", func(*args, **kwargs)) + finally: + response_mode.reset(token) + + return wrapped + + +def async_to_raw_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]: + @functools.wraps(func) + async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]: + token = response_mode.set("raw") + try: + return cast("AsyncAPIResponse[R]", await func(*args, **kwargs)) + finally: + response_mode.reset(token) + + return wrapped + + +class ResponseContextManager(Generic[R]): + """Sends the request on ``__enter__`` and closes the response on ``__exit__``.""" + + def __init__(self, call: Callable[[], APIResponse[R]]) -> None: + self._call = call + self._response: APIResponse[R] | None = None + + def __enter__(self) -> APIResponse[R]: + token = response_mode.set("stream") + try: + self._response = self._call() + finally: + response_mode.reset(token) + return self._response + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + if self._response is not None: + self._response.close() + + +class AsyncResponseContextManager(Generic[R]): + """Sends the request on ``__aenter__`` and closes the response on ``__aexit__``.""" + + def __init__(self, call: Callable[[], Awaitable[AsyncAPIResponse[R]]]) -> None: + self._call = call + self._response: AsyncAPIResponse[R] | None = None + + async def __aenter__(self) -> AsyncAPIResponse[R]: + token = response_mode.set("stream") + try: + self._response = await self._call() + finally: + response_mode.reset(token) + return self._response + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + if self._response is not None: + await self._response.close() + + +def to_streamed_response_wrapper( + func: Callable[P, R], +) -> Callable[P, ResponseContextManager[R]]: + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[R]: + return ResponseContextManager( + lambda: cast("APIResponse[R]", func(*args, **kwargs)), + ) + + return wrapped + + +def async_to_streamed_response_wrapper( + func: Callable[P, Awaitable[R]], +) -> Callable[P, AsyncResponseContextManager[R]]: + @functools.wraps(func) + def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[R]: + async def call() -> AsyncAPIResponse[R]: + return cast("AsyncAPIResponse[R]", await func(*args, **kwargs)) + + return AsyncResponseContextManager(call) + + return wrapped diff --git a/src/didit/_types.py b/src/didit/_types.py new file mode 100644 index 0000000..e9b52c3 --- /dev/null +++ b/src/didit/_types.py @@ -0,0 +1,72 @@ +"""Shared helper types: the NOT_GIVEN sentinel, request option types and file inputs.""" + +from __future__ import annotations + +import os +from collections.abc import Mapping +from typing import IO, TYPE_CHECKING, Literal, Union + +import httpx +from typing_extensions import final, override + +__all__ = [ + "NOT_GIVEN", + "Body", + "FileContent", + "FileTypes", + "Headers", + "NotGiven", + "Query", + "Timeout", +] + + +@final +class NotGiven: + """Sentinel for "the caller did not pass this argument". + + It distinguishes an omitted field (left out of the request) from an explicit + ``None`` (sent as JSON ``null``). + """ + + _instance: NotGiven | None = None + + def __new__(cls) -> NotGiven: + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __bool__(self) -> Literal[False]: + return False + + @override + def __repr__(self) -> str: + return "NOT_GIVEN" + + def __reduce__(self) -> str: + return "NOT_GIVEN" + + +NOT_GIVEN = NotGiven() +"""The value of every optional argument that was not passed.""" + +Timeout = Union[float, httpx.Timeout, None] +Headers = Mapping[str, Union[str, None]] +"""Extra request headers. A ``None`` value removes a header the SDK would send.""" +Query = Mapping[str, object] +Body = Mapping[str, object] +"""Extra top-level JSON body fields.""" + +if TYPE_CHECKING: + FileContent = Union[IO[bytes], bytes, bytearray, "os.PathLike[str]"] +else: + FileContent = Union[IO[bytes], bytes, bytearray, os.PathLike] +"""A file: a path, the bytes, or a binary stream.""" + +FileTypes = Union[ + FileContent, + tuple[Union[str, None], FileContent], + tuple[Union[str, None], FileContent, Union[str, None]], +] +"""A file, optionally with a file name and a content type: ``content``, +``(filename, content)`` or ``(filename, content, content_type)``.""" diff --git a/src/didit/_utils.py b/src/didit/_utils.py new file mode 100644 index 0000000..21c9d33 --- /dev/null +++ b/src/didit/_utils.py @@ -0,0 +1,143 @@ +"""Small helpers shared by the core: request encoding and environment lookups.""" + +from __future__ import annotations + +import datetime as _dt +import json +import os +import platform +import urllib.parse +from collections.abc import Iterable, Mapping +from typing import Any, cast + +import pydantic + +from ._exceptions import DiditError +from ._files import HttpxFile, to_httpx_file +from ._types import NotGiven +from ._version import __version__ + +__all__ = [ + "encode_json", + "encode_path", + "env", + "is_given", + "multipart_fields", + "query_params", + "strip_not_given", + "user_agent", +] + + +def is_given(value: object) -> bool: + return not isinstance(value, NotGiven) + + +def env(name: str) -> str | None: + value = os.environ.get(name) + if value is None: + return None + value = value.strip() + return value or None + + +def strip_not_given(value: object) -> Any: + """Drop omitted (``NOT_GIVEN``) values, encode dates as ISO 8601 text and turn + response models back into the JSON the API sent (so a result can be posted again).""" + if isinstance(value, pydantic.BaseModel): + return value.model_dump(mode="json", by_alias=True, exclude_unset=True, warnings=False) + if isinstance(value, Mapping): + items = cast("Mapping[str, object]", value).items() + return {k: strip_not_given(v) for k, v in items if not isinstance(v, NotGiven)} + if isinstance(value, (list, tuple)): + return [strip_not_given(v) for v in cast("Iterable[object]", value)] + if isinstance(value, (_dt.datetime, _dt.date)): + return value.isoformat() + return value + + +def encode_json(value: object) -> bytes: + """Compact JSON for a request body; raises :class:`DiditError` for values JSON + cannot carry (``NaN``, ``Infinity``, arbitrary objects) before anything is sent.""" + try: + return json.dumps(strip_not_given(value), separators=(",", ":"), allow_nan=False).encode() + except (TypeError, ValueError) as exc: + raise DiditError(f"The request body cannot be sent as JSON: {exc}") from exc + + +def _scalar_text(value: object) -> str: + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (_dt.datetime, _dt.date)): + return value.isoformat() + return str(value) + + +def query_params(params: Mapping[str, object] | None) -> list[tuple[str, str]]: + """Encode query parameters; ``NOT_GIVEN`` and ``None`` are left out.""" + out: list[tuple[str, str]] = [] + if not params: + return out + for key, value in params.items(): + if value is None or isinstance(value, NotGiven): + continue + if isinstance(value, (list, tuple)): + out.extend((key, _scalar_text(v)) for v in cast("Iterable[object]", value)) + else: + out.append((key, _scalar_text(value))) + return out + + +def multipart_fields( + body: Mapping[str, object], file_fields: Iterable[str] +) -> tuple[dict[str, str], list[tuple[str, HttpxFile]]]: + """Split a request body into multipart text fields and files. + + Objects and arrays are sent as JSON-encoded text fields, which is how the + Didit API reads structured values in multipart requests. + """ + names = set(file_fields) + data: dict[str, str] = {} + files: list[tuple[str, HttpxFile]] = [] + for key, value in body.items(): + if value is None or isinstance(value, NotGiven): + continue + if key in names: + if isinstance(value, list): + files.extend((key, to_httpx_file(v)) for v in value) # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType] + else: + files.append((key, to_httpx_file(cast("Any", value)))) + elif isinstance(value, (Mapping, list, tuple)): + data[key] = encode_json(cast(object, value)).decode() + else: + data[key] = _scalar_text(value) + return data, files + + +def encode_path(template: str, params: Mapping[str, object]) -> str: + """Fill ``{name}`` placeholders with percent-encoded values. + + Empty values and the dot segments ``.`` and ``..`` are refused so that a + caller-supplied id can never change which endpoint is called. + """ + out = template + for name, value in params.items(): + if isinstance(value, NotGiven) or value is None: + raise ValueError(f"The path parameter {name!r} is required.") + text = _scalar_text(value) + if text in ("", ".", ".."): + raise ValueError(f"The path parameter {name!r} must be a non-empty id, got {text!r}.") + out = out.replace("{" + name + "}", urllib.parse.quote(text, safe="")) + return out + + +def user_agent() -> str: + system = platform.system() or "unknown" + return ( + f"didit-python/{__version__} " + f"({platform.python_implementation()} {platform.python_version()}; {system})" + ) + + +def client_header() -> str: + return f"python/{__version__}" diff --git a/src/didit/_version.py b/src/didit/_version.py new file mode 100644 index 0000000..580804a --- /dev/null +++ b/src/didit/_version.py @@ -0,0 +1,2 @@ +__title__ = "didit" +__version__ = "0.1.0" # x-release-please-version diff --git a/src/didit/authentication/__init__.py b/src/didit/authentication/__init__.py new file mode 100644 index 0000000..381c88c --- /dev/null +++ b/src/didit/authentication/__init__.py @@ -0,0 +1,75 @@ +"""Didit Authentication for your backend (docs/DESIGN.md section 10). + + from didit.authentication import Authentication, TokenVerificationError + + auth = Authentication.from_publishable_key("pk_live_...", client_id=APPLICATION_ID) + try: + claims = auth.verify_access_token(access_token) + except TokenVerificationError as exc: + ... # exc.reason is machine readable, for example "expired" + +Each application has its own OpenID Connect issuer (``https://.auth.didit.me`` +or its custom domain). This module verifies the ES256 tokens it signs, exchanges +and refreshes codes, checks and revokes sessions and runs step-ups. It never +uses your Didit API key. Token verification needs ``pip install 'didit-sdk[auth]'``. +""" + +from ._client import ( + DEFAULT_LEEWAY, + DEFAULT_TIMEOUT, + Amount, + AsyncAuthentication, + Audience, + Authentication, +) +from ._errors import TokenVerificationError, TokenVerificationReason +from ._pkce import PkcePair, create_nonce, create_pkce, create_state +from ._types import ( + AccessTokenClaims, + ActorClaim, + AuthenticationSession, + IdTokenClaims, + OrganizationClaim, + OrganizationRole, + SessionCustomRole, + SessionOrganization, + StepUp, + StepUpAction, + StepUpMethod, + StepUpRule, + StepUpStatus, + TokenResponse, + VerificationLevel, + VerifiedClaims, +) + +__all__ = [ + "DEFAULT_LEEWAY", + "DEFAULT_TIMEOUT", + "AccessTokenClaims", + "ActorClaim", + "Amount", + "AsyncAuthentication", + "Audience", + "Authentication", + "AuthenticationSession", + "IdTokenClaims", + "OrganizationClaim", + "OrganizationRole", + "PkcePair", + "SessionCustomRole", + "SessionOrganization", + "StepUp", + "StepUpAction", + "StepUpMethod", + "StepUpRule", + "StepUpStatus", + "TokenResponse", + "TokenVerificationError", + "TokenVerificationReason", + "VerificationLevel", + "VerifiedClaims", + "create_nonce", + "create_pkce", + "create_state", +] diff --git a/src/didit/authentication/_cache.py b/src/didit/authentication/_cache.py new file mode 100644 index 0000000..e920987 --- /dev/null +++ b/src/didit/authentication/_cache.py @@ -0,0 +1,241 @@ +"""Caching of the issuer's discovery document and key set (no I/O here). + +Both are cached for the response's ``Cache-Control: max-age`` (Didit sends +``public, max-age=300``), five minutes when the header is absent, never less than +a minute and never more than a day. A token whose ``kid`` is not in the cached +key set triggers a refetch only when the last fetch, successful or failed, is at +least a minute old. After a failed fetch, verification that needs the keys fails +fast for a minute (as a connection error) instead of fetching again on every token. +""" + +from __future__ import annotations + +import contextlib +import time +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, cast + +import httpx + +from ._crypto import load_p256_key +from ._errors import TokenVerificationError +from ._jwt import p256_point + +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey + +__all__ = [ + "DEFAULT_MAX_AGE", + "MAX_MAX_AGE", + "MIN_MAX_AGE", + "REFETCH_INTERVAL", + "Discovery", + "IssuerCache", + "KeySet", + "cache_seconds", + "parse_discovery", + "parse_key_set", +] + +DEFAULT_MAX_AGE = 300.0 +MIN_MAX_AGE = 60.0 +MAX_MAX_AGE = 86400.0 +REFETCH_INTERVAL = 60.0 + + +def cache_seconds(headers: httpx.Headers) -> float: + """How long a discovery or JWKS response may be kept. + + ``max-age`` (less the ``Age`` a shared cache reports), or zero for + ``no-store`` and ``no-cache``, or five minutes without either; then clamped + to at least 60 seconds and at most a day. + """ + max_age: float | None = None + for directive in (headers.get("cache-control") or "").split(","): + name, _, value = directive.partition("=") + name = name.strip().lower() + if name in ("no-store", "no-cache"): + max_age = 0.0 + break + if name == "max-age": + try: + max_age = float(int(value.strip().strip('"'))) + except ValueError: + continue + if max_age is None: + seconds = DEFAULT_MAX_AGE + else: + seconds = max_age + with contextlib.suppress(ValueError): + seconds -= int(headers.get("age") or 0) + return min(MAX_MAX_AGE, max(MIN_MAX_AGE, seconds)) + + +@dataclass(frozen=True) +class Discovery: + """The parts of ``/.well-known/openid-configuration`` the SDK uses.""" + + issuer: str | None + token_endpoint: str | None + jwks_uri: str | None + + +def _absolute_url(value: object) -> str | None: + if not isinstance(value, str): + return None + try: + url = httpx.URL(value) + except httpx.InvalidURL: + return None + return value if url.scheme in ("https", "http") and url.host else None + + +def parse_discovery(data: object) -> Discovery | None: + """The discovery document, or ``None`` when ``data`` is not a JSON object.""" + if not isinstance(data, dict): + return None + document = cast("dict[str, Any]", data) + issuer = document.get("issuer") + return Discovery( + issuer=issuer if isinstance(issuer, str) else None, + token_endpoint=_absolute_url(document.get("token_endpoint")), + jwks_uri=_absolute_url(document.get("jwks_uri")), + ) + + +@dataclass +class KeySet: + """The issuer's published keys by ``kid``, with the public keys built so far.""" + + keys: dict[str, Mapping[str, Any]] + _loaded: dict[str, EllipticCurvePublicKey] = field(default_factory=lambda: {}, repr=False) + + def public_key(self, kid: str) -> EllipticCurvePublicKey: + """The P-256 key published as ``kid``, or ``TokenVerificationError("unknown_key")``.""" + key = self._loaded.get(kid) + if key is not None: + return key + jwk = self.keys.get(kid) + point = p256_point(jwk) if jwk is not None else None + if point is not None: + try: + key = load_p256_key(*point) + except ValueError: + key = None + if key is None: + raise TokenVerificationError( + "unknown_key", + "The issuer publishes no ES256 (P-256) key with the token's key id (kid).", + ) + self._loaded[kid] = key + return key + + +def parse_key_set(data: object) -> KeySet | None: + """The key set, or ``None`` when ``data`` is not a JWKS (``{"keys": [...]}``). + + Keys without a string ``kid`` are ignored: Didit always sets one, and a token + is matched to its key by ``kid`` only. + """ + if not isinstance(data, dict): + return None + keys = cast("dict[str, Any]", data).get("keys") + if not isinstance(keys, list): + return None + by_kid: dict[str, Mapping[str, Any]] = {} + for item in cast("list[object]", keys): + if isinstance(item, dict): + jwk = cast("dict[str, Any]", item) + kid = jwk.get("kid") + if isinstance(kid, str) and kid and kid not in by_kid: + by_kid[kid] = jwk + return KeySet(keys=by_kid) + + +@dataclass +class _Entry: + value: Any + expires_at: float + + +class IssuerCache: + """What is cached for one issuer, and when to fetch again. + + The sync and async clients hold their own lock around a fetch; reads of a + fresh entry need no lock, because entries are replaced, never mutated. + """ + + clock: Callable[[], float] + + def __init__(self, clock: Callable[[], float] = time.monotonic) -> None: + self.clock = clock + self._discovery: _Entry | None = None + self._key_set: _Entry | None = None + self._key_set_attempted_at: float | None = None + self._key_set_failed = False + + def discovery(self) -> Discovery | None: + """The cached discovery document while it is fresh.""" + entry = self._discovery + if entry is None or self.clock() >= entry.expires_at: + return None + return cast("Discovery", entry.value) + + def store_discovery(self, discovery: Discovery, headers: httpx.Headers) -> Discovery: + self._discovery = _Entry(discovery, self.clock() + cache_seconds(headers)) + return discovery + + def key_set(self) -> KeySet | None: + """The cached key set while it is fresh.""" + entry = self._key_set + if entry is None or self.clock() >= entry.expires_at: + return None + return cast("KeySet", entry.value) + + def key_set_fetch_failed(self) -> None: + """Note a fetch of the key set (or the discovery before it) that failed.""" + self._key_set_attempted_at = self.clock() + self._key_set_failed = True + + def key_set_unavailable(self) -> bool: + """No key set is fresh and the last fetch failed less than a minute ago.""" + attempted_at = self._key_set_attempted_at + return ( + self._key_set_failed + and self.key_set() is None + and attempted_at is not None + and self.clock() - attempted_at < REFETCH_INTERVAL + ) + + def store_key_set(self, key_set: KeySet, headers: httpx.Headers) -> KeySet: + now = self.clock() + self._key_set = _Entry(key_set, now + cache_seconds(headers)) + self._key_set_attempted_at = now + self._key_set_failed = False + return key_set + + def key_set_with(self, kid: str) -> KeySet | None: + """The fresh key set when it has ``kid``: the lock-free fast path.""" + key_set = self.key_set() + return key_set if key_set is not None and kid in key_set.keys else None + + def needs_key_set_fetch(self, kid: str) -> bool: + """Whether verifying a token signed with ``kid`` should fetch the key set: + when none is fresh, or when ``kid`` is unknown and the last fetch, + successful or failed, was at least :data:`REFETCH_INTERVAL` seconds ago. + Check :meth:`key_set_unavailable` first.""" + key_set = self.key_set() + if key_set is None: + return True + if kid in key_set.keys: + return False + attempted_at = self._key_set_attempted_at + return attempted_at is None or self.clock() - attempted_at >= REFETCH_INTERVAL + + def current_key_set(self) -> KeySet: + """The fresh key set, after :meth:`needs_key_set_fetch` said no fetch is needed.""" + key_set = self.key_set() + if key_set is None: # pragma: no cover - needs_key_set_fetch() is True then + raise RuntimeError("no key set is cached") + return key_set diff --git a/src/didit/authentication/_client.py b/src/didit/authentication/_client.py new file mode 100644 index 0000000..e4101c7 --- /dev/null +++ b/src/didit/authentication/_client.py @@ -0,0 +1,881 @@ +"""``Authentication`` and ``AsyncAuthentication`` (docs/DESIGN.md section 10). + +Everything that decides what is sent, what is cached and whether a token is +valid lives in :class:`_AuthenticationBase` and the pure helpers it calls; +:class:`Authentication` and :class:`AsyncAuthentication` only differ in how they +send a request and which lock guards a fetch. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import re +import threading +import time +import urllib.parse +import weakref +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from decimal import Decimal, InvalidOperation +from types import TracebackType +from typing import Any, TypeVar, Union, cast + +import httpx +from typing_extensions import Self + +from .._exceptions import ( + APIConnectionError, + APIResponseValidationError, + APIStatusError, + APITimeoutError, + ConfigurationError, + _request_id, # pyright: ignore[reportPrivateUsage] + make_status_error, +) +from .._models import BaseModel, validate_response +from .._utils import client_header, encode_path, user_agent +from ._cache import Discovery, IssuerCache, KeySet, parse_discovery, parse_key_set +from ._crypto import verify_es256 +from ._errors import TokenVerificationError +from ._issuer import issuer_from_publishable_key, normalise_issuer +from ._jwt import ParsedToken, TokenKind, check_claims, parse_token +from ._pkce import create_nonce as _create_nonce +from ._pkce import create_pkce as _create_pkce +from ._pkce import create_state as _create_state +from ._types import ( + AccessTokenClaims, + AuthenticationSession, + IdTokenClaims, + StepUp, + StepUpAction, + TokenResponse, +) + +__all__ = [ + "DEFAULT_LEEWAY", + "DEFAULT_TIMEOUT", + "Amount", + "AsyncAuthentication", + "Audience", + "Authentication", +] + +log = logging.getLogger("didit") + +DEFAULT_TIMEOUT = 10.0 +DEFAULT_LEEWAY = 60 +_DISCOVERY_PATH = ".well-known/openid-configuration" +_JWKS_PATH = ".well-known/jwks.json" +_TOKEN_PATH = "token/" # noqa: S105 - a path, not a secret +_SESSION_PATH = "session/" +_STEP_UP_PATH = "step-up/" + +Audience = Union[str, Sequence[str]] +"""One audience (the application id) or several, any of which is accepted.""" +Amount = Union[str, int, float, Decimal] +"""A money amount; sent as a decimal string.""" + +ModelT = TypeVar("ModelT", bound=BaseModel) + +# The issuer could not give its discovery document or keys. +_FETCH_ERRORS = (APIConnectionError, APIStatusError, APIResponseValidationError) + + +@dataclass(frozen=True) +class _Call: + """One request to the issuer, before it is bound to an HTTP client.""" + + method: str + url: str + bearer: str | None = None + form: Mapping[str, str] | None = None + json_body: Mapping[str, object] | None = None + secrets: tuple[str, ...] = () + """Values redacted from error messages: tokens, codes and verifiers.""" + + +def _required_text(value: object, name: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{name} is empty.") + return value.strip() + + +_DECIMAL_TEXT = re.compile(r"-?[0-9]+(\.[0-9]+)?\Z") + + +def _amount_text(amount: Amount) -> str: + if isinstance(amount, bool) or not isinstance(amount, (str, int, float, Decimal)): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError("amount must be a decimal string, an int, a float or a Decimal.") + if isinstance(amount, str): + # Sent as written, so only plain ASCII decimals ("1500.00"): no exponents, + # underscores, other digits or NaN, which the issuer would read differently. + text = amount.strip() + if not _DECIMAL_TEXT.match(text): + raise ValueError(f"amount must be a decimal number such as '1500.00', got {amount!r}.") + return text + try: + value = Decimal(repr(amount)) if isinstance(amount, float) else Decimal(amount) + except InvalidOperation as exc: # pragma: no cover - every int, float and Decimal parses + raise ValueError(f"amount must be a decimal number, got {amount!r}.") from exc + if not value.is_finite(): + raise ValueError(f"amount must be a finite number, got {amount!r}.") + return format(value, "f") + + +def _kept_request(call: _Call, request: httpx.Request) -> httpx.Request: + """The request an error or response keeps: without the form body of a token + request, which carries the code, the PKCE verifier or the refresh token.""" + if call.form is None: + return request + headers = request.headers.copy() + headers.pop("content-length", None) + return httpx.Request(request.method, request.url, headers=headers) + + +class _AuthenticationBase: + """Configuration, request building, response parsing and token checks shared by + the sync and async classes. Nothing here does I/O.""" + + issuer: str + """The issuer every token must name in ``iss``, without a trailing slash.""" + client_id: str | None + """The application id: sent with token requests and the default audience.""" + timeout: float | httpx.Timeout + leeway: float + """Seconds of clock skew allowed for ``exp``, ``nbf`` and ``iat``.""" + + create_pkce = staticmethod(_create_pkce) + create_state = staticmethod(_create_state) + create_nonce = staticmethod(_create_nonce) + + def __init__( + self, + issuer: str, + *, + client_id: str | None, + timeout: float | httpx.Timeout, + leeway: float, + ) -> None: + self.issuer = normalise_issuer(issuer) + if client_id is not None and (not isinstance(client_id, str) or not client_id.strip()): # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError( + "client_id is empty: pass your application id or leave it out." + ) + self.client_id = client_id.strip() if client_id else None + if isinstance(leeway, bool) or not isinstance(leeway, (int, float)) or not leeway >= 0: # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError("leeway must be 0 or more seconds.") + self.leeway = float(leeway) + self.timeout = timeout + self._cache = IssuerCache() + self._now: Callable[[], float] = time.time + + def __repr__(self) -> str: + return f"{type(self).__name__}(issuer={self.issuer!r}, client_id={self.client_id!r})" + + # Requests --------------------------------------------------------------- + + def _url(self, path: str) -> str: + return f"{self.issuer}/{path}" + + def _discovery_call(self) -> _Call: + return _Call("GET", self._url(_DISCOVERY_PATH)) + + def _jwks_call(self, discovery: Discovery) -> _Call: + return _Call("GET", discovery.jwks_uri or self._url(_JWKS_PATH)) + + @staticmethod + def _exchange_form( + code: str, code_verifier: str, redirect_uri: str + ) -> tuple[dict[str, str], tuple[str, ...]]: + code = _required_text(code, "code") + code_verifier = _required_text(code_verifier, "code_verifier") + form = { + "grant_type": "authorization_code", + "code": code, + "code_verifier": code_verifier, + "redirect_uri": _required_text(redirect_uri, "redirect_uri"), + } + return form, (code, code_verifier) + + @staticmethod + def _refresh_form(refresh_token: str) -> tuple[dict[str, str], tuple[str, ...]]: + refresh_token = _required_text(refresh_token, "refresh_token") + return {"grant_type": "refresh_token", "refresh_token": refresh_token}, (refresh_token,) + + def _token_call( + self, discovery: Discovery, form: dict[str, str], secrets: tuple[str, ...] + ) -> _Call: + if self.client_id: + form = {**form, "client_id": self.client_id} + url = discovery.token_endpoint or self._url(_TOKEN_PATH) + return _Call("POST", url, form=form, secrets=secrets) + + def _session_call(self, method: str, access_token: str) -> _Call: + token = _required_text(access_token, "access_token") + return _Call(method, self._url(_SESSION_PATH), bearer=token, secrets=(token,)) + + def _create_step_up_call( + self, + access_token: str, + *, + action: StepUpAction | str, + amount: Amount | None, + currency: str | None, + return_to: str | None, + ) -> _Call: + token = _required_text(access_token, "access_token") + body: dict[str, object] = {"action": _required_text(action, "action")} + if amount is not None: + body["amount"] = _amount_text(amount) + if currency is not None: + body["currency"] = currency + if return_to is not None: + body["return_to"] = return_to + return _Call( + "POST", self._url(_STEP_UP_PATH), bearer=token, json_body=body, secrets=(token,) + ) + + def _retrieve_step_up_call(self, access_token: str, step_up_id: str) -> _Call: + token = _required_text(access_token, "access_token") + path = encode_path(_STEP_UP_PATH + "{step_up_id}/", {"step_up_id": step_up_id}) + return _Call("GET", self._url(path), bearer=token, secrets=(token,)) + + def _build(self, call: _Call, client: httpx.Client | httpx.AsyncClient) -> httpx.Request: + headers = { + "Accept": "application/json", + "User-Agent": user_agent(), + "X-Didit-Client": client_header(), + } + if call.bearer is not None: + headers["Authorization"] = f"Bearer {call.bearer}" + content: bytes | None = None + if call.form is not None: + headers["Content-Type"] = "application/x-www-form-urlencoded" + content = urllib.parse.urlencode(call.form).encode("ascii") + elif call.json_body is not None: + headers["Content-Type"] = "application/json" + content = json.dumps(call.json_body, separators=(",", ":")).encode("utf-8") + return client.build_request( + call.method, call.url, headers=headers, content=content, timeout=self.timeout + ) + + # Responses -------------------------------------------------------------- + + @staticmethod + def _transport_error(exc: httpx.HTTPError, request: httpx.Request) -> APIConnectionError: + if isinstance(exc, httpx.TimeoutException): + return APITimeoutError(request=request) + return APIConnectionError(request=request) + + @staticmethod + def _log(request: httpx.Request, response: httpx.Response, started: float) -> None: + if log.isEnabledFor(logging.DEBUG): + log.debug( + "%s %s -> %d in %.0f ms (request id %s)", + request.method, + request.url.path, + response.status_code, + (time.monotonic() - started) * 1000, + _request_id(response.headers) or "-", + ) + + @staticmethod + def _read(call: _Call, response: httpx.Response) -> object: + if not response.is_success: + body: object + try: + body = response.json() + except ValueError: + body = response.text or None + raise make_status_error(response, body, redact=call.secrets) + if response.status_code == 204 or not response.content: + return None + try: + return response.json() + except ValueError as exc: + raise APIResponseValidationError( + response=response, + message=( + f"Expected a JSON response from {call.method} {httpx.URL(call.url).path}, " + f"got {response.headers.get('content-type', 'no content type')}." + ), + ) from exc + + def _model(self, call: _Call, response: httpx.Response, model: type[ModelT]) -> ModelT: + data = self._read(call, response) + if not isinstance(data, dict): + raise APIResponseValidationError( + response=response, + message=f"Expected a JSON object from {call.method} {httpx.URL(call.url).path}.", + ) + return cast("ModelT", validate_response(model, cast("dict[str, Any]", data))) + + def _store_discovery(self, call: _Call, response: httpx.Response) -> Discovery: + discovery = parse_discovery(self._read(call, response)) + if discovery is None: + raise APIResponseValidationError( + response=response, message="The issuer's discovery document is not a JSON object." + ) + if discovery.issuer is None: + raise APIResponseValidationError( + response=response, + message=f"The discovery document at {call.url} names no issuer.", + ) + if discovery.issuer != self.issuer: + raise ConfigurationError( + f"The discovery document at {call.url} names the issuer {discovery.issuer!r}, " + f"not {self.issuer!r}. Configure Authentication with {discovery.issuer!r} " + "(an application's issuer changes when its custom domain is activated)." + ) + # Codes and refresh tokens go to the token endpoint, and the keys decide which + # tokens are trusted: neither may downgrade to plain http, and tokens never + # leave the issuer's own origin. + issuer = httpx.URL(self.issuer) + for name, value, same_origin in ( + ("token_endpoint", discovery.token_endpoint, True), + ("jwks_uri", discovery.jwks_uri, False), + ): + if value is None: + continue + url = httpx.URL(value) + if url.scheme != "https" and url.scheme != issuer.scheme: + raise APIResponseValidationError( + response=response, + message=f"The discovery document's {name} {value!r} does not use https.", + ) + if same_origin and (url.scheme, url.host, url.port) != ( + issuer.scheme, + issuer.host, + issuer.port, + ): + raise APIResponseValidationError( + response=response, + message=( + f"The discovery document's {name} {value!r} is not on the issuer's " + f"origin {self.issuer!r}." + ), + ) + return self._cache.store_discovery(discovery, response.headers) + + def _keys_unavailable(self) -> APIConnectionError: + discovery = self._cache.discovery() + url = (discovery.jwks_uri if discovery else None) or self._url(_JWKS_PATH) + return APIConnectionError( + request=httpx.Request("GET", url), + message=( + "The issuer's keys could not be fetched less than a minute ago; the next " + "attempt is made once that minute has passed." + ), + ) + + def _store_key_set(self, call: _Call, response: httpx.Response) -> KeySet: + key_set = parse_key_set(self._read(call, response)) + if key_set is None: + raise APIResponseValidationError( + response=response, message='The issuer\'s JWKS is not a key set ({"keys": [...]}).' + ) + return self._cache.store_key_set(key_set, response.headers) + + # Token checks ----------------------------------------------------------- + + def _audience(self, audience: Audience | None, *, required: bool) -> tuple[str, ...] | None: + if audience is None: + if self.client_id: + return (self.client_id,) + if required: + raise ConfigurationError( + "verify_id_token needs the audience: pass audience=, " + "or client_id= to the Authentication client." + ) + return None + values = (audience,) if isinstance(audience, str) else tuple(audience) + if not values or any(not isinstance(v, str) or not v for v in values): # pyright: ignore[reportUnnecessaryIsInstance] + raise ValueError("audience must be a non-empty string or a list of them.") + return values + + @staticmethod + def _nonce(nonce: str | None) -> str | None: + if nonce is not None and (not isinstance(nonce, str) or not nonce): # pyright: ignore[reportUnnecessaryIsInstance] + raise ValueError("nonce is empty: pass the nonce of this sign-in, or None.") + return nonce + + def _finish( + self, + parsed: ParsedToken, + key_set: KeySet, + model: type[ModelT], + *, + kind: TokenKind, + audience: tuple[str, ...] | None, + nonce: str | None, + ) -> ModelT: + key = key_set.public_key(parsed.kid) + if not verify_es256(key, parsed.signing_input, parsed.signature): + raise TokenVerificationError( + "invalid_signature", "The token's signature does not verify with the issuer's key." + ) + check_claims( + parsed.claims, + kind=kind, + issuer=self.issuer, + audience=audience, + nonce=nonce, + leeway=self.leeway, + now=self._now(), + ) + return cast("ModelT", validate_response(model, parsed.claims)) + + +class Authentication(_AuthenticationBase): + """Didit Authentication for your backend: verify the tokens your application's + issuer signs and call its token, session and step-up endpoints. + + from didit.authentication import Authentication + + auth = Authentication.from_publishable_key("pk_live_...", client_id=APPLICATION_ID) + claims = auth.verify_access_token(access_token) + print(claims.sub, claims.vendor_data) + + It never takes or sends your Didit API key. Discovery and the issuer's keys + are fetched on first use and cached; one instance per process is the + intended use and is thread safe. Token verification needs the ``auth`` + extra: ``pip install 'didit-sdk[auth]'``. + """ + + _client: httpx.Client + + def __init__( + self, + issuer: str, + *, + client_id: str | None = None, + http_client: httpx.Client | None = None, + timeout: float | httpx.Timeout = DEFAULT_TIMEOUT, + leeway: float = DEFAULT_LEEWAY, + ) -> None: + """``issuer`` is the application's issuer: ``https://.auth.didit.me``, + ``https://.sandbox.auth.didit.me`` or its active custom domain. + ``client_id`` is the application id; when set it is sent with token + requests (the issuer checks it) and is the default audience.""" + super().__init__(issuer, client_id=client_id, timeout=timeout, leeway=leeway) + if http_client is not None and not isinstance(http_client, httpx.Client): # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError( + "http_client must be an httpx.Client (use AsyncAuthentication for httpx.AsyncClient)." + ) + self._owns_client = http_client is None + self._client = http_client or httpx.Client(follow_redirects=False) + self._lock = threading.Lock() + + @classmethod + def from_publishable_key( + cls, + publishable_key: str, + *, + client_id: str | None = None, + http_client: httpx.Client | None = None, + timeout: float | httpx.Timeout = DEFAULT_TIMEOUT, + leeway: float = DEFAULT_LEEWAY, + ) -> Self: + """The client for the issuer a ``pk_live_`` or ``pk_test_`` publishable key carries.""" + return cls( + issuer_from_publishable_key(publishable_key), + client_id=client_id, + http_client=http_client, + timeout=timeout, + leeway=leeway, + ) + + def close(self) -> None: + """Close the HTTP connection pool (only if this client created it).""" + if self._owns_client: + self._client.close() + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close() + + # Tokens ----------------------------------------------------------------- + + def verify_access_token( + self, token: str, audience: Audience | None = None + ) -> AccessTokenClaims: + """Verify an access token offline and return its claims. + + Checks, in order: the header (``alg`` ES256 only, so ``none``, HS*, RS*, + PS*, EdDSA and every other algorithm are refused; ``typ`` ``at+jwt``), the + ES256 signature against the issuer's published key with the token's + ``kid``, then ``iss`` (equal to the issuer), ``sub``, ``exp``, ``nbf`` and + ``iat`` (with ``leeway`` seconds of clock skew) and, when ``audience`` is + given or ``client_id`` is configured, ``aud``. + + An offline check cannot see a session revoked a moment ago: access + tokens expire within five minutes. Call ``get_session`` when the action + needs an instant revocation check. + + Raises :class:`TokenVerificationError` with a machine-readable ``reason``. + """ + expected = self._audience(audience, required=False) + parsed = parse_token(token, "access") + key_set = self._key_set_for(parsed.kid) + return self._finish( + parsed, key_set, AccessTokenClaims, kind="access", audience=expected, nonce=None + ) + + def verify_id_token( + self, token: str, *, nonce: str | None, audience: Audience | None = None + ) -> IdTokenClaims: + """Verify an ID token and return its claims. + + The same checks as :meth:`verify_access_token` (``typ`` must be ``JWT`` or + absent, never ``at+jwt``), plus ``aud``, which is always checked: pass + ``audience`` or configure ``client_id``. ``nonce`` must equal the + ``nonce`` you sent with the authorization request; pass ``None`` only for + an ID token returned by :meth:`refresh`, which carries no nonce. + """ + expected = self._audience(audience, required=True) + nonce = self._nonce(nonce) + parsed = parse_token(token, "id") + key_set = self._key_set_for(parsed.kid) + return self._finish( + parsed, key_set, IdTokenClaims, kind="id", audience=expected, nonce=nonce + ) + + def exchange_code(self, code: str, *, code_verifier: str, redirect_uri: str) -> TokenResponse: + """Exchange an authorization code for tokens (``POST token/``, form encoded). + + Check the callback's ``state`` first. The code is single use and expires + after 60 seconds; ``code_verifier`` is the PKCE verifier of this sign-in + and ``redirect_uri`` must be exactly the one the sign-in used. Token + requests are never retried. + """ + form, secrets = self._exchange_form(code, code_verifier, redirect_uri) + call = self._token_call(self._discovery(), form, secrets) + return self._model(call, self._send(call), TokenResponse) + + def refresh(self, refresh_token: str) -> TokenResponse: + """Swap a refresh token for new tokens (``POST token/``, form encoded). + + Refresh tokens are single use: replace both tokens with the result. + Serialise refreshes for one session (one at a time, across processes + and browser tabs), because presenting a refresh token that was already + used revokes the whole session. For the same reason this request is + never retried; if its outcome is unknown, sign the person in again. + """ + form, secrets = self._refresh_form(refresh_token) + call = self._token_call(self._discovery(), form, secrets) + return self._model(call, self._send(call), TokenResponse) + + # Sessions and step-ups -------------------------------------------------- + + def get_session(self, access_token: str) -> AuthenticationSession: + """The live session behind ``access_token`` (``GET session/``). + + The instant revocation check: unlike :meth:`verify_access_token` it fails + (``AuthenticationError``, HTTP 401) as soon as the session is revoked, + expired or idle, or the token was replaced by a refresh. Agent access + tokens are refused here. + """ + call = self._session_call("GET", access_token) + return self._model(call, self._send(call), AuthenticationSession) + + def revoke_session(self, access_token: str) -> None: + """Sign the person out: revoke the session behind ``access_token`` (``DELETE session/``).""" + call = self._session_call("DELETE", access_token) + self._read(call, self._send(call)) + + def create_step_up( + self, + access_token: str, + *, + action: StepUpAction | str, + amount: Amount | None = None, + currency: str | None = None, + return_to: str | None = None, + ) -> StepUp: + """Ask whether the person must verify before a sensitive action (``POST step-up/``). + + ``action`` is ``payout`` (with ``amount`` and an optional three-letter + ``currency``), ``bank`` or ``contact``. ``return_to`` is where the person + comes back after verifying and must be one of the application's + registered redirect URIs; the issuer currently requires it on every + call and answers HTTP 400 (``BadRequestError``) without it. + + ``required=False`` means no rule applies and the action may go ahead. + Otherwise send the person to ``verification_url``; they return to + ``return_to`` with ``step_up=&status=approved``, which proves + nothing: call :meth:`retrieve_step_up` and act only on status + ``approved``. + """ + call = self._create_step_up_call( + access_token, action=action, amount=amount, currency=currency, return_to=return_to + ) + return self._model(call, self._send(call), StepUp) + + def retrieve_step_up(self, access_token: str, step_up_id: str) -> StepUp: + """Read a step-up (``GET step-up/{id}/``) with a token of the session that + asked for it. Act only when its status is ``approved`` (:attr:`StepUp.approved`); + ``pending``, ``review``, ``declined``, ``expired`` and ``superseded`` all mean no.""" + call = self._retrieve_step_up_call(access_token, step_up_id) + return self._model(call, self._send(call), StepUp) + + # I/O ---------------------------------------------------------------------- + + def _send(self, call: _Call) -> httpx.Response: + request = self._build(call, self._client) + started = time.monotonic() + try: + response = self._client.send(request, follow_redirects=False) + except httpx.HTTPError as exc: + raise self._transport_error(exc, _kept_request(call, request)) from exc + self._log(request, response, started) + response.request = _kept_request(call, request) + return response + + def _discovery(self) -> Discovery: + discovery = self._cache.discovery() + if discovery is not None: + return discovery + with self._lock: + return self._discovery_locked() + + def _discovery_locked(self) -> Discovery: + discovery = self._cache.discovery() + if discovery is None: + call = self._discovery_call() + discovery = self._store_discovery(call, self._send(call)) + return discovery + + def _key_set_for(self, kid: str) -> KeySet: + key_set = self._cache.key_set_with(kid) + if key_set is not None: + return key_set + with self._lock: + if self._cache.key_set_unavailable(): + raise self._keys_unavailable() + if not self._cache.needs_key_set_fetch(kid): + return self._cache.current_key_set() + try: + call = self._jwks_call(self._discovery_locked()) + return self._store_key_set(call, self._send(call)) + except _FETCH_ERRORS: + # Counted only when the issuer failed: not for a cancelled call + # and not for a configuration error, which is raised every time. + self._cache.key_set_fetch_failed() + raise + + +class AsyncAuthentication(_AuthenticationBase): + """The asyncio variant of :class:`Authentication`, on ``httpx.AsyncClient``. + + auth = AsyncAuthentication.from_publishable_key("pk_live_...", client_id=APPLICATION_ID) + claims = await auth.verify_access_token(access_token) + + Every method has the same arguments and checks as on :class:`Authentication`. + """ + + _client: httpx.AsyncClient + + def __init__( + self, + issuer: str, + *, + client_id: str | None = None, + http_client: httpx.AsyncClient | None = None, + timeout: float | httpx.Timeout = DEFAULT_TIMEOUT, + leeway: float = DEFAULT_LEEWAY, + ) -> None: + super().__init__(issuer, client_id=client_id, timeout=timeout, leeway=leeway) + if http_client is not None and not isinstance(http_client, httpx.AsyncClient): # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError( + "http_client must be an httpx.AsyncClient (use Authentication for httpx.Client)." + ) + self._owns_client = http_client is None + self._client = http_client or httpx.AsyncClient(follow_redirects=False) + # An asyncio.Lock belongs to one event loop; keep one per loop so that the + # instance also works across several asyncio.run() calls. + self._locks: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Lock] = ( + weakref.WeakKeyDictionary() + ) + + @classmethod + def from_publishable_key( + cls, + publishable_key: str, + *, + client_id: str | None = None, + http_client: httpx.AsyncClient | None = None, + timeout: float | httpx.Timeout = DEFAULT_TIMEOUT, + leeway: float = DEFAULT_LEEWAY, + ) -> Self: + """The client for the issuer a ``pk_live_`` or ``pk_test_`` publishable key carries.""" + return cls( + issuer_from_publishable_key(publishable_key), + client_id=client_id, + http_client=http_client, + timeout=timeout, + leeway=leeway, + ) + + async def close(self) -> None: + """Close the HTTP connection pool (only if this client created it).""" + if self._owns_client: + await self._client.aclose() + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self.close() + + # Tokens ----------------------------------------------------------------- + + async def verify_access_token( + self, token: str, audience: Audience | None = None + ) -> AccessTokenClaims: + """Verify an access token offline and return its claims. + + Checks, in order: the header (``alg`` ES256 only, so ``none``, HS*, RS*, + PS*, EdDSA and every other algorithm are refused; ``typ`` ``at+jwt``), the + ES256 signature against the issuer's published key with the token's + ``kid``, then ``iss`` (equal to the issuer), ``sub``, ``exp``, ``nbf`` and + ``iat`` (with ``leeway`` seconds of clock skew) and, when ``audience`` is + given or ``client_id`` is configured, ``aud``. + + An offline check cannot see a session revoked a moment ago: access + tokens expire within five minutes. Call ``get_session`` when the action + needs an instant revocation check. + + Raises :class:`TokenVerificationError` with a machine-readable ``reason``. + """ + expected = self._audience(audience, required=False) + parsed = parse_token(token, "access") + key_set = await self._key_set_for(parsed.kid) + return self._finish( + parsed, key_set, AccessTokenClaims, kind="access", audience=expected, nonce=None + ) + + async def verify_id_token( + self, token: str, *, nonce: str | None, audience: Audience | None = None + ) -> IdTokenClaims: + """See :meth:`Authentication.verify_id_token`.""" + expected = self._audience(audience, required=True) + nonce = self._nonce(nonce) + parsed = parse_token(token, "id") + key_set = await self._key_set_for(parsed.kid) + return self._finish( + parsed, key_set, IdTokenClaims, kind="id", audience=expected, nonce=nonce + ) + + async def exchange_code( + self, code: str, *, code_verifier: str, redirect_uri: str + ) -> TokenResponse: + """See :meth:`Authentication.exchange_code`. Never retried.""" + form, secrets = self._exchange_form(code, code_verifier, redirect_uri) + call = self._token_call(await self._discovery(), form, secrets) + return self._model(call, await self._send(call), TokenResponse) + + async def refresh(self, refresh_token: str) -> TokenResponse: + """See :meth:`Authentication.refresh`: serialise refreshes for one session, + because reusing a consumed refresh token revokes it. Never retried.""" + form, secrets = self._refresh_form(refresh_token) + call = self._token_call(await self._discovery(), form, secrets) + return self._model(call, await self._send(call), TokenResponse) + + # Sessions and step-ups -------------------------------------------------- + + async def get_session(self, access_token: str) -> AuthenticationSession: + """See :meth:`Authentication.get_session`.""" + call = self._session_call("GET", access_token) + return self._model(call, await self._send(call), AuthenticationSession) + + async def revoke_session(self, access_token: str) -> None: + """See :meth:`Authentication.revoke_session`.""" + call = self._session_call("DELETE", access_token) + self._read(call, await self._send(call)) + + async def create_step_up( + self, + access_token: str, + *, + action: StepUpAction | str, + amount: Amount | None = None, + currency: str | None = None, + return_to: str | None = None, + ) -> StepUp: + """See :meth:`Authentication.create_step_up`: the issuer currently requires + ``return_to``, and the backend acts only on status ``approved``.""" + call = self._create_step_up_call( + access_token, action=action, amount=amount, currency=currency, return_to=return_to + ) + return self._model(call, await self._send(call), StepUp) + + async def retrieve_step_up(self, access_token: str, step_up_id: str) -> StepUp: + """See :meth:`Authentication.retrieve_step_up`: act only on status ``approved``.""" + call = self._retrieve_step_up_call(access_token, step_up_id) + return self._model(call, await self._send(call), StepUp) + + # I/O ---------------------------------------------------------------------- + + async def _send(self, call: _Call) -> httpx.Response: + request = self._build(call, self._client) + started = time.monotonic() + try: + response = await self._client.send(request, follow_redirects=False) + except httpx.HTTPError as exc: + raise self._transport_error(exc, _kept_request(call, request)) from exc + self._log(request, response, started) + response.request = _kept_request(call, request) + return response + + def _async_lock(self) -> asyncio.Lock: + loop = asyncio.get_running_loop() + lock = self._locks.get(loop) + if lock is None: + lock = self._locks.setdefault(loop, asyncio.Lock()) + return lock + + async def _discovery(self) -> Discovery: + discovery = self._cache.discovery() + if discovery is not None: + return discovery + async with self._async_lock(): + return await self._discovery_locked() + + async def _discovery_locked(self) -> Discovery: + discovery = self._cache.discovery() + if discovery is None: + call = self._discovery_call() + discovery = self._store_discovery(call, await self._send(call)) + return discovery + + async def _key_set_for(self, kid: str) -> KeySet: + key_set = self._cache.key_set_with(kid) + if key_set is not None: + return key_set + async with self._async_lock(): + if self._cache.key_set_unavailable(): + raise self._keys_unavailable() + if not self._cache.needs_key_set_fetch(kid): + return self._cache.current_key_set() + try: + call = self._jwks_call(await self._discovery_locked()) + return self._store_key_set(call, await self._send(call)) + except _FETCH_ERRORS: + # Counted only when the issuer failed: not for a cancelled call + # and not for a configuration error, which is raised every time. + self._cache.key_set_fetch_failed() + raise diff --git a/src/didit/authentication/_crypto.py b/src/didit/authentication/_crypto.py new file mode 100644 index 0000000..64e69cb --- /dev/null +++ b/src/didit/authentication/_crypto.py @@ -0,0 +1,49 @@ +"""ES256 (ECDSA P-256 with SHA-256) signature checks. + +``cryptography`` is an optional extra (``pip install 'didit-sdk[auth]'``), so it is +imported here, when a signature is first checked, and never when +``didit.authentication`` is imported. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey + +__all__ = ["INSTALL_HINT", "load_p256_key", "verify_es256"] + +INSTALL_HINT = ( + "Verifying Didit Authentication tokens needs the cryptography package: " + "pip install 'didit-sdk[auth]'." +) + + +def load_p256_key(x: int, y: int) -> EllipticCurvePublicKey: + """The P-256 public key at point ``(x, y)``; :class:`ValueError` if the point is not on the curve.""" + try: + from cryptography.hazmat.primitives.asymmetric import ec + except ImportError as exc: + raise ImportError(INSTALL_HINT) from exc + return ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key() + + +def verify_es256(key: EllipticCurvePublicKey, signing_input: bytes, signature: bytes) -> bool: + """Whether ``signature`` (JWS form: the 32-byte ``r`` then the 32-byte ``s``) signs ``signing_input``.""" + try: + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature + except ImportError as exc: + raise ImportError(INSTALL_HINT) from exc + if len(signature) != 64: + return False + r = int.from_bytes(signature[:32], "big") + s = int.from_bytes(signature[32:], "big") + try: + key.verify(encode_dss_signature(r, s), signing_input, ec.ECDSA(hashes.SHA256())) + except InvalidSignature: + return False + return True diff --git a/src/didit/authentication/_errors.py b/src/didit/authentication/_errors.py new file mode 100644 index 0000000..f76b310 --- /dev/null +++ b/src/didit/authentication/_errors.py @@ -0,0 +1,40 @@ +"""The error raised when an Authentication token fails verification (docs/DESIGN.md section 10).""" + +from __future__ import annotations + +from typing import Literal + +from .._exceptions import DiditError + +__all__ = ["TokenVerificationError", "TokenVerificationReason"] + +TokenVerificationReason = Literal[ + "malformed", + "unsupported_algorithm", + "invalid_type", + "unknown_key", + "invalid_signature", + "invalid_issuer", + "invalid_audience", + "expired", + "not_yet_valid", + "invalid_issued_at", + "invalid_nonce", + "missing_claim", +] + + +class TokenVerificationError(DiditError): + """An access token or ID token failed verification: do not trust any of its claims. + + ``reason`` is machine readable: ``malformed``, ``unsupported_algorithm``, + ``invalid_type``, ``unknown_key``, ``invalid_signature``, ``invalid_issuer``, + ``invalid_audience``, ``expired``, ``not_yet_valid``, ``invalid_issued_at``, + ``invalid_nonce`` or ``missing_claim``. The message never contains the token. + """ + + reason: TokenVerificationReason + + def __init__(self, reason: TokenVerificationReason, message: str) -> None: + super().__init__(message) + self.reason = reason diff --git a/src/didit/authentication/_issuer.py b/src/didit/authentication/_issuer.py new file mode 100644 index 0000000..22b044f --- /dev/null +++ b/src/didit/authentication/_issuer.py @@ -0,0 +1,97 @@ +"""The issuer URL and the publishable key that carries it. + +An application's issuer is its canonical origin: ``https://.auth.didit.me`` +(live), ``https://.sandbox.auth.didit.me`` (sandbox) or its active custom +domain, with no trailing slash. A publishable key is ``pk_live_`` (live) or +``pk_test_`` (sandbox) followed by the unpadded base64url encoding of the issuer. +""" + +from __future__ import annotations + +import base64 +import binascii +import re +import urllib.parse + +from .._exceptions import ConfigurationError + +__all__ = ["issuer_from_publishable_key", "normalise_issuer"] + +_BASE64URL = re.compile(r"[A-Za-z0-9_-]+") +_LOOPBACK_HOSTS = frozenset({"localhost", "127.0.0.1", "::1"}) +_PREFIXES = {"pk_live_": False, "pk_test_": True} + + +def _is_loopback(host: str) -> bool: + return host in _LOOPBACK_HOSTS or host.endswith(".localhost") + + +def normalise_issuer(issuer: object, *, allow_loopback_http: bool = True) -> str: + """The issuer without a trailing slash, or :class:`ConfigurationError`. + + The issuer must be an absolute ``https`` URL with no credentials, query or + fragment. Plain ``http`` is accepted only for ``localhost`` and loopback + addresses (a local test stack) when ``allow_loopback_http`` is true. + """ + if not isinstance(issuer, str) or not issuer.strip(): + raise ConfigurationError( + "The issuer is empty: pass your application's issuer " + "(https://.auth.didit.me or your custom domain) or use " + "Authentication.from_publishable_key(...)." + ) + text = issuer.strip() + parts: urllib.parse.SplitResult | None + try: + parts = urllib.parse.urlsplit(text) + host = parts.hostname + _ = parts.port + except ValueError: + parts, host = None, None + if parts is not None and (parts.username is not None or parts.password is not None): + raise ConfigurationError("The issuer URL must not contain a user name or password.") + if ( + parts is None + or not host + or "?" in text + or "#" in text + or any(ch.isspace() or ord(ch) < 32 for ch in text) + ): + raise ConfigurationError( + f"The issuer must be an absolute https URL with no query or fragment, got {text!r}." + ) + secure = parts.scheme == "https" + local = allow_loopback_http and parts.scheme == "http" and _is_loopback(host) + if not secure and not local: + raise ConfigurationError( + f"The issuer must use https (http only for localhost while testing), got {text!r}." + ) + return text.rstrip("/") + + +def issuer_from_publishable_key(publishable_key: object) -> str: + """The issuer a ``pk_live_`` or ``pk_test_`` publishable key carries. + + A live key must name an ``https`` issuer; a test (sandbox) key may also name + ``http`` on localhost, as the console allows for sandbox applications. + """ + key = publishable_key.strip() if isinstance(publishable_key, str) else "" + prefix = key[:8] + if prefix not in _PREFIXES: + raise ConfigurationError( + "A publishable key starts with pk_live_ or pk_test_; copy it from the " + "Developers section of Authentication in the Didit console." + ) + encoded = key[8:] + issuer: str | None = None + if _BASE64URL.fullmatch(encoded) and len(encoded) % 4 != 1: + try: + raw = base64.urlsafe_b64decode(encoded + "=" * (-len(encoded) % 4)) + issuer = raw.decode("utf-8") + except (binascii.Error, UnicodeDecodeError): + issuer = None + if issuer is None: + raise ConfigurationError( + f"The publishable key is not valid: expected {prefix} followed by the " + "base64url-encoded issuer." + ) + return normalise_issuer(issuer, allow_loopback_http=_PREFIXES[prefix]) diff --git a/src/didit/authentication/_jwt.py b/src/didit/authentication/_jwt.py new file mode 100644 index 0000000..f0b1663 --- /dev/null +++ b/src/didit/authentication/_jwt.py @@ -0,0 +1,242 @@ +"""JWS parsing and claim checks for Didit Authentication tokens (no I/O here). + +Didit issuers sign with ES256 only (``service-didit-auth`` +``authentication/services/issuer.py``): access tokens carry the header +``typ: at+jwt`` and ID tokens ``typ: JWT``. Every other algorithm, including +``none``, is refused before any key is looked up. +""" + +from __future__ import annotations + +import base64 +import binascii +import hmac +import json +import math +import re +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Any, Literal, NoReturn + +from ._errors import TokenVerificationError, TokenVerificationReason + +__all__ = [ + "ALGORITHM", + "ParsedToken", + "TokenKind", + "check_claims", + "p256_point", + "parse_token", +] + +ALGORITHM = "ES256" +MAX_TOKEN_LENGTH = 16384 +TokenKind = Literal["access", "id"] + +_ACCESS_TYPES = frozenset({"at+jwt", "application/at+jwt"}) +_ID_TYPES = frozenset({"jwt", "application/jwt"}) +_BASE64URL = re.compile(r"[A-Za-z0-9_-]*") + + +@dataclass(frozen=True) +class ParsedToken: + """A JWS whose header passed the algorithm and type checks; not yet verified.""" + + header: dict[str, Any] + claims: dict[str, Any] + signing_input: bytes + signature: bytes + kid: str + + +def _fail(reason: TokenVerificationReason, message: str) -> NoReturn: + raise TokenVerificationError(reason, message) + + +def _shown(value: object) -> str: + """A short, safe rendering of a header or claim value for an error message.""" + text = repr(value) + return text if len(text) <= 64 else text[:61] + "..." + + +def b64url_decode(segment: str) -> bytes | None: + """Unpadded base64url (RFC 7515 section 2), or ``None`` when ``segment`` is not.""" + if not _BASE64URL.fullmatch(segment) or len(segment) % 4 == 1: + return None + try: + return base64.urlsafe_b64decode(segment + "=" * (-len(segment) % 4)) + except (binascii.Error, ValueError): # pragma: no cover - excluded by the pattern above + return None + + +def _no_duplicates(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + out: dict[str, Any] = {} + for key, value in pairs: + if key in out: + raise ValueError(f"duplicate member {key!r}") + out[key] = value + return out + + +def _no_constants(name: str) -> NoReturn: + raise ValueError(f"{name} is not JSON") + + +def _json_object(segment: str, part: str) -> dict[str, Any]: + raw = b64url_decode(segment) + if raw is None: + _fail("malformed", f"The token {part} is not base64url.") + try: + value: object = json.loads( + raw.decode("utf-8"), object_pairs_hook=_no_duplicates, parse_constant=_no_constants + ) + except (UnicodeDecodeError, ValueError, RecursionError): + _fail("malformed", f"The token {part} is not valid JSON.") + if not isinstance(value, dict): + _fail("malformed", f"The token {part} is not a JSON object.") + return value # pyright: ignore[reportUnknownVariableType] + + +def _check_type(header: Mapping[str, Any], kind: TokenKind) -> None: + typ = header.get("typ") + if kind == "access": + if not isinstance(typ, str) or typ.lower() not in _ACCESS_TYPES: + _fail( + "invalid_type", + f"The token is not an access token: its header typ is {_shown(typ)}, not 'at+jwt'.", + ) + return + # ID tokens are signed with typ JWT; refusing at+jwt keeps an access token from passing as one. + if typ is not None and (not isinstance(typ, str) or typ.lower() not in _ID_TYPES): + _fail( + "invalid_type", + f"The token is not an ID token: its header typ is {_shown(typ)}, not 'JWT'.", + ) + + +def parse_token(token: object, kind: TokenKind) -> ParsedToken: + """Split and decode a compact JWS and check its header (``alg``, ``typ``, ``kid``). + + Raises :class:`TokenVerificationError`; nothing here is trusted until the + signature has been checked. + """ + if not isinstance(token, str) or not token: + _fail("malformed", "The token is empty or not a string.") + if len(token) > MAX_TOKEN_LENGTH: + _fail("malformed", f"The token is longer than {MAX_TOKEN_LENGTH} characters.") + parts = token.split(".") + if len(parts) != 3: + _fail("malformed", "The token is not a compact JWS (three dot-separated segments).") + header = _json_object(parts[0], "header") + alg = header.get("alg") + if alg != ALGORITHM: + _fail( + "unsupported_algorithm", + f"The token is signed with {_shown(alg)}; only ES256 is accepted.", + ) + if "crit" in header: + _fail( + "malformed", "The token header has critical extensions (crit), which are not supported." + ) + _check_type(header, kind) + kid = header.get("kid") + if not isinstance(kid, str) or not kid: + _fail("malformed", "The token header names no signing key (kid).") + claims = _json_object(parts[1], "payload") + signature = b64url_decode(parts[2]) + if signature is None: + _fail("malformed", "The token signature is not base64url.") + return ParsedToken( + header=header, + claims=claims, + signing_input=f"{parts[0]}.{parts[1]}".encode("ascii"), + signature=signature, + kid=kid, + ) + + +def p256_point(jwk: Mapping[str, Any]) -> tuple[int, int] | None: + """The ``(x, y)`` point of a P-256 signing key in JWK form, or ``None`` when the + key is anything else (another curve or key type, or not meant for ES256 signatures).""" + if jwk.get("kty") != "EC" or jwk.get("crv") != "P-256": + return None + if jwk.get("use", "sig") != "sig" or jwk.get("alg", ALGORITHM) != ALGORITHM: + return None + coordinates: list[int] = [] + for name in ("x", "y"): + value = jwk.get(name) + raw = b64url_decode(value) if isinstance(value, str) else None + if raw is None or len(raw) != 32: + return None + coordinates.append(int.from_bytes(raw, "big")) + return coordinates[0], coordinates[1] + + +def _numeric_date(claims: Mapping[str, Any], name: str) -> float: + if name not in claims: + _fail("missing_claim", f"The token has no {name} claim.") + value = claims[name] + seconds = math.nan + if not isinstance(value, bool) and isinstance(value, (int, float)): + try: + seconds = float(value) + except OverflowError: # an integer beyond any float + seconds = math.nan + if not math.isfinite(seconds): + _fail("malformed", f"The token's {name} claim is not a number of seconds.") + return seconds + + +def _audiences(value: object) -> list[str]: + if isinstance(value, str): + return [value] + if isinstance(value, list): + return [item for item in value if isinstance(item, str)] # pyright: ignore[reportUnknownVariableType] + return [] + + +def check_claims( + claims: Mapping[str, Any], + *, + kind: TokenKind, + issuer: str, + audience: tuple[str, ...] | None, + nonce: str | None, + leeway: float, + now: float, +) -> None: + """Check the registered claims of a token whose signature is already verified. + + ``iss`` must equal ``issuer``; ``sub``, ``exp`` and ``iat`` must be present; + ``exp``, ``nbf`` and ``iat`` are compared with ``now`` allowing ``leeway`` + seconds of clock skew; ``aud`` must contain one of ``audience`` when it is + given; for an ID token ``nonce`` must equal ``nonce`` when it is given. + """ + iss = claims.get("iss") + if iss is None: + _fail("missing_claim", "The token has no iss claim.") + if iss != issuer: + _fail("invalid_issuer", f"The token was issued by {_shown(iss)}, not by {issuer!r}.") + sub = claims.get("sub") + if not isinstance(sub, str) or not sub: + _fail("missing_claim", "The token has no sub claim.") + exp = _numeric_date(claims, "exp") + iat = _numeric_date(claims, "iat") + nbf = _numeric_date(claims, "nbf") if "nbf" in claims else None + if now >= exp + leeway: + _fail("expired", "The token has expired.") + if nbf is not None and now + leeway < nbf: + _fail("not_yet_valid", "The token is not valid yet (nbf is in the future).") + if now + leeway < iat: + _fail("invalid_issued_at", "The token was issued in the future (iat).") + if audience is not None: + if "aud" not in claims: + _fail("missing_claim", "The token has no aud claim.") + if not set(_audiences(claims["aud"])).intersection(audience): + _fail("invalid_audience", "The token was not issued for this audience.") + if kind == "id" and nonce is not None: + token_nonce = claims.get("nonce") + if not isinstance(token_nonce, str) or not hmac.compare_digest( + token_nonce.encode("utf-8"), nonce.encode("utf-8") + ): + _fail("invalid_nonce", "The ID token's nonce does not match this sign-in.") diff --git a/src/didit/authentication/_pkce.py b/src/didit/authentication/_pkce.py new file mode 100644 index 0000000..d524845 --- /dev/null +++ b/src/didit/authentication/_pkce.py @@ -0,0 +1,52 @@ +"""PKCE (RFC 7636), ``state`` and ``nonce`` values for the authorization code flow.""" + +from __future__ import annotations + +import base64 +import hashlib +import secrets +from dataclasses import dataclass, field +from typing import Literal + +__all__ = ["PkcePair", "create_nonce", "create_pkce", "create_state", "s256_challenge"] + +# 32 random octets, base64url encoded: the 43-character verifier RFC 7636 section 4.1 recommends. +_RANDOM_BYTES = 32 + + +@dataclass(frozen=True) +class PkcePair: + """A PKCE verifier and its S256 challenge. + + Send ``challenge`` and ``method`` with the authorization request, keep + ``verifier`` on your server (it is left out of ``repr``) and pass it to + ``exchange_code``. + """ + + verifier: str = field(repr=False) + challenge: str + method: Literal["S256"] = "S256" + + +def s256_challenge(verifier: str) -> str: + """``BASE64URL(SHA256(ASCII(verifier)))`` without padding (RFC 7636 section 4.2).""" + digest = hashlib.sha256(verifier.encode("ascii")).digest() + return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii") + + +def create_pkce() -> PkcePair: + """A new random PKCE verifier (43 characters from ``secrets``) and its S256 challenge.""" + verifier = secrets.token_urlsafe(_RANDOM_BYTES) + return PkcePair(verifier=verifier, challenge=s256_challenge(verifier)) + + +def create_state() -> str: + """A new random ``state`` value. Store it with the sign-in attempt and compare it + with the ``state`` on the callback before exchanging the code.""" + return secrets.token_urlsafe(_RANDOM_BYTES) + + +def create_nonce() -> str: + """A new random ``nonce`` value. Send it with the authorization request and pass it + to ``verify_id_token``, which checks the ID token carries the same value.""" + return secrets.token_urlsafe(_RANDOM_BYTES) diff --git a/src/didit/authentication/_types.py b/src/didit/authentication/_types.py new file mode 100644 index 0000000..b137505 --- /dev/null +++ b/src/didit/authentication/_types.py @@ -0,0 +1,196 @@ +"""Token claims and issuer responses of Didit Authentication (docs/DESIGN.md section 10). + +Shapes follow ``service-didit-auth`` (``authentication/services/issuer.py``, +``services/tokens.py``, ``services/step_ups.py`` and ``views/runtime.py``). +Every model keeps fields and claims this version does not know (``model_extra``). +""" + +from __future__ import annotations + +import datetime as _dt +from typing import Any, Literal + +from pydantic import Field + +from .._models import BaseModel + +__all__ = [ + "AccessTokenClaims", + "ActorClaim", + "AuthenticationSession", + "IdTokenClaims", + "OrganizationClaim", + "OrganizationRole", + "SessionCustomRole", + "SessionOrganization", + "StepUp", + "StepUpAction", + "StepUpMethod", + "StepUpRule", + "StepUpStatus", + "TokenResponse", + "VerificationLevel", + "VerifiedClaims", +] + +OrganizationRole = Literal["admin", "member", "billing"] +VerificationLevel = Literal["liveness", "liveness_face_match", "full_id"] +StepUpAction = Literal["payout", "bank", "contact"] +StepUpRule = Literal["payout", "device", "bank", "contact"] +StepUpMethod = Literal["liveness", "selfie", "full_id"] +StepUpStatus = Literal["pending", "review", "approved", "declined", "expired", "superseded"] + + +class OrganizationClaim(BaseModel): + """``org``: the person's earliest organization membership, when customer organizations are on.""" + + id: str + role: OrganizationRole | str | None = None + + +class ActorClaim(BaseModel): + """``act``: the agent acting for the person. Only agent access tokens carry it.""" + + sub: str + """The agent id.""" + agent: str | None = None + """The agent's name.""" + scopes: list[str] | None = None + """What the person allowed the agent to do; never act beyond these.""" + + +class VerifiedClaims(BaseModel): + """``verified``: the highest approved verification level and the claims the + application selected. Empty when the person has not been verified.""" + + level: VerificationLevel | str | None = None + over_18: bool | None = None + over_21: bool | None = None + given_name: str | None = None + family_name: str | None = None + birthdate: str | None = None + nationality: str | None = None + country: str | None = None + document_verified: bool | None = None + liveness: bool | None = None + + +class _TokenClaims(BaseModel): + iss: str + """The issuer.""" + sub: str + """The identity id.""" + aud: str | list[str] | None = None + """The application id.""" + sid: str | None = None + """The session id (the grant id on an agent token).""" + jti: str | None = None + iat: int + exp: int + nbf: int | None = None + vendor_data: str | None = None + """The person's reference across Didit products (verification sessions, monitoring).""" + amr: list[str] | None = None + """How the person signed in, for example ``["passkey"]``; empty on an agent token.""" + org: OrganizationClaim | None = None + verified: VerifiedClaims | None = None + act: ActorClaim | None = None + """``None`` for a person; the agent on an agent access token.""" + + +class AccessTokenClaims(_TokenClaims): + """The verified claims of an access token (header ``typ: at+jwt``).""" + + client_id: str | None = None + """The application id.""" + + +class IdTokenClaims(_TokenClaims): + """The verified claims of an ID token (header ``typ: JWT``).""" + + auth_time: int | None = None + nonce: str | None = None + email: str | None = None + email_verified: bool | None = None + name: str | None = None + phone_number: str | None = None + + +class TokenResponse(BaseModel): + """``POST token/``: new tokens. The tokens are left out of ``repr``; never log them.""" + + access_token: str = Field(repr=False) + """A JWT access token that expires within five minutes.""" + token_type: str + expires_in: int + """Seconds until ``access_token`` expires.""" + refresh_token: str | None = Field(default=None, repr=False) + """Opaque and single use: every refresh returns a new one.""" + id_token: str | None = Field(default=None, repr=False) + session_id: str | None = None + + +class SessionCustomRole(BaseModel): + key: str | None = None + permissions: list[str] | None = None + + +class SessionOrganization(BaseModel): + organization_id: str | None = None + organization_name: str | None = Field(default=None, alias="organization__name") + """Sent by the API as ``organization__name``.""" + role: OrganizationRole | str | None = None + custom_role: SessionCustomRole | None = None + + +class AuthenticationSession(BaseModel): + """``GET session/``: the live session behind an access token.""" + + sub: str + """The identity id.""" + sid: str + """The session id.""" + application_id: str | None = None + vendor_data: str | None = None + amr: list[str] | None = None + mfa_verified: bool | None = None + email: str | None = None + email_verified: bool | None = None + phone: str | None = None + verified_identity: dict[str, Any] | None = None + verified: VerifiedClaims | None = None + expires_at: _dt.datetime | None = None + post_logout_redirect_uri: str | None = None + organizations: list[SessionOrganization] | None = None + + +class StepUp(BaseModel): + """A step-up: ``POST step-up/`` and ``GET step-up/{id}/``. + + When ``POST step-up/`` finds no rule that applies it answers only + ``required=False``: the action may go ahead. Otherwise send the person to + ``verification_url``, then read the step-up again with ``retrieve_step_up`` + and act only when :attr:`approved` is true. + """ + + required: bool | None = None + """``POST step-up/`` only: whether the person must verify before the action.""" + id: str | None = None + rule: StepUpRule | str | None = None + method: StepUpMethod | str | None = None + status: StepUpStatus | str | None = None + level: VerificationLevel | str | None = None + amount: str | None = None + currency: str | None = None + created_at: _dt.datetime | None = None + completed_at: _dt.datetime | None = None + expires_at: _dt.datetime | None = None + verification_url: str | None = None + """``POST step-up/`` only: where to send the person to verify.""" + expires_in: int | None = None + """``POST step-up/`` only: seconds the person has to verify.""" + + @property + def approved(self) -> bool: + """``True`` only for status ``approved``: the one status to act on.""" + return self.status == "approved" diff --git a/src/didit/py.typed b/src/didit/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/didit/resources/__init__.py b/src/didit/resources/__init__.py new file mode 100644 index 0000000..c4e297c --- /dev/null +++ b/src/didit/resources/__init__.py @@ -0,0 +1,463 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The resource tree of the Didit API (docs/DESIGN.md section 4).""" + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from didit._base_client import AsyncAPIClient, SyncAPIClient + +if TYPE_CHECKING: + from didit.resources.account import Account, AsyncAccount + from didit.resources.age_estimation import AgeEstimation, AsyncAgeEstimation + from didit.resources.aml import Aml, AsyncAml + from didit.resources.aml_monitoring import AmlMonitoring, AsyncAmlMonitoring + from didit.resources.billing import AsyncBilling, Billing + from didit.resources.biometric_templates import AsyncBiometricTemplates, BiometricTemplates + from didit.resources.business_sessions import AsyncBusinessSessions, BusinessSessions + from didit.resources.businesses import AsyncBusinesses, Businesses + from didit.resources.cases import AsyncCases, Cases + from didit.resources.customization import AsyncCustomization, Customization + from didit.resources.database_validation import AsyncDatabaseValidation, DatabaseValidation + from didit.resources.document_ai import AsyncDocumentAi, DocumentAi + from didit.resources.email import AsyncEmail, Email + from didit.resources.face_match import AsyncFaceMatch, FaceMatch + from didit.resources.face_search import AsyncFaceSearch, FaceSearch + from didit.resources.fraud_checks import AsyncFraudChecks, FraudChecks + from didit.resources.id_verification import AsyncIdVerification, IdVerification + from didit.resources.id_wallets import AsyncIdWallets, IdWallets + from didit.resources.ip import AsyncIp, Ip + from didit.resources.kyb import AsyncKyb, Kyb + from didit.resources.networks import AsyncNetworks, Networks + from didit.resources.passive_liveness import AsyncPassiveLiveness, PassiveLiveness + from didit.resources.phone import AsyncPhone, Phone + from didit.resources.proof_of_address import AsyncProofOfAddress, ProofOfAddress + from didit.resources.questionnaires import AsyncQuestionnaires, Questionnaires + from didit.resources.regulatory_reports import AsyncRegulatoryReports, RegulatoryReports + from didit.resources.report_templates import AsyncReportTemplates, ReportTemplates + from didit.resources.sessions import AsyncSessions, Sessions + from didit.resources.transactions import AsyncTransactions, Transactions + from didit.resources.travel_rule import AsyncTravelRule, TravelRule + from didit.resources.users import AsyncUsers, Users + from didit.resources.wallet_screening import AsyncWalletScreening, WalletScreening + from didit.resources.webhooks import AsyncWebhooks, Webhooks + from didit.resources.workflows import AsyncWorkflows, Workflows + +__all__ = ["AsyncRootResources", "SyncRootResources"] + + +class SyncRootResources(SyncAPIClient): + """Top-level resources, loaded on first use.""" + + @cached_property + def account(self) -> Account: + from didit.resources.account import Account + + return Account(self) + + @cached_property + def age_estimation(self) -> AgeEstimation: + from didit.resources.age_estimation import AgeEstimation + + return AgeEstimation(self) + + @cached_property + def aml(self) -> Aml: + from didit.resources.aml import Aml + + return Aml(self) + + @cached_property + def aml_monitoring(self) -> AmlMonitoring: + from didit.resources.aml_monitoring import AmlMonitoring + + return AmlMonitoring(self) + + @cached_property + def billing(self) -> Billing: + from didit.resources.billing import Billing + + return Billing(self) + + @cached_property + def biometric_templates(self) -> BiometricTemplates: + from didit.resources.biometric_templates import BiometricTemplates + + return BiometricTemplates(self) + + @cached_property + def business_sessions(self) -> BusinessSessions: + from didit.resources.business_sessions import BusinessSessions + + return BusinessSessions(self) + + @cached_property + def businesses(self) -> Businesses: + from didit.resources.businesses import Businesses + + return Businesses(self) + + @cached_property + def cases(self) -> Cases: + from didit.resources.cases import Cases + + return Cases(self) + + @cached_property + def customization(self) -> Customization: + from didit.resources.customization import Customization + + return Customization(self) + + @cached_property + def database_validation(self) -> DatabaseValidation: + from didit.resources.database_validation import DatabaseValidation + + return DatabaseValidation(self) + + @cached_property + def document_ai(self) -> DocumentAi: + from didit.resources.document_ai import DocumentAi + + return DocumentAi(self) + + @cached_property + def email(self) -> Email: + from didit.resources.email import Email + + return Email(self) + + @cached_property + def face_match(self) -> FaceMatch: + from didit.resources.face_match import FaceMatch + + return FaceMatch(self) + + @cached_property + def face_search(self) -> FaceSearch: + from didit.resources.face_search import FaceSearch + + return FaceSearch(self) + + @cached_property + def fraud_checks(self) -> FraudChecks: + from didit.resources.fraud_checks import FraudChecks + + return FraudChecks(self) + + @cached_property + def id_verification(self) -> IdVerification: + from didit.resources.id_verification import IdVerification + + return IdVerification(self) + + @cached_property + def id_wallets(self) -> IdWallets: + from didit.resources.id_wallets import IdWallets + + return IdWallets(self) + + @cached_property + def ip(self) -> Ip: + from didit.resources.ip import Ip + + return Ip(self) + + @cached_property + def kyb(self) -> Kyb: + from didit.resources.kyb import Kyb + + return Kyb(self) + + @cached_property + def networks(self) -> Networks: + from didit.resources.networks import Networks + + return Networks(self) + + @cached_property + def passive_liveness(self) -> PassiveLiveness: + from didit.resources.passive_liveness import PassiveLiveness + + return PassiveLiveness(self) + + @cached_property + def phone(self) -> Phone: + from didit.resources.phone import Phone + + return Phone(self) + + @cached_property + def proof_of_address(self) -> ProofOfAddress: + from didit.resources.proof_of_address import ProofOfAddress + + return ProofOfAddress(self) + + @cached_property + def questionnaires(self) -> Questionnaires: + from didit.resources.questionnaires import Questionnaires + + return Questionnaires(self) + + @cached_property + def regulatory_reports(self) -> RegulatoryReports: + from didit.resources.regulatory_reports import RegulatoryReports + + return RegulatoryReports(self) + + @cached_property + def report_templates(self) -> ReportTemplates: + from didit.resources.report_templates import ReportTemplates + + return ReportTemplates(self) + + @cached_property + def sessions(self) -> Sessions: + from didit.resources.sessions import Sessions + + return Sessions(self) + + @cached_property + def transactions(self) -> Transactions: + from didit.resources.transactions import Transactions + + return Transactions(self) + + @cached_property + def travel_rule(self) -> TravelRule: + from didit.resources.travel_rule import TravelRule + + return TravelRule(self) + + @cached_property + def users(self) -> Users: + from didit.resources.users import Users + + return Users(self) + + @cached_property + def wallet_screening(self) -> WalletScreening: + from didit.resources.wallet_screening import WalletScreening + + return WalletScreening(self) + + @cached_property + def webhooks(self) -> Webhooks: + from didit.resources.webhooks import Webhooks + + return Webhooks(self) + + @cached_property + def workflows(self) -> Workflows: + from didit.resources.workflows import Workflows + + return Workflows(self) + + +class AsyncRootResources(AsyncAPIClient): + """Top-level resources, loaded on first use.""" + + @cached_property + def account(self) -> AsyncAccount: + from didit.resources.account import AsyncAccount + + return AsyncAccount(self) + + @cached_property + def age_estimation(self) -> AsyncAgeEstimation: + from didit.resources.age_estimation import AsyncAgeEstimation + + return AsyncAgeEstimation(self) + + @cached_property + def aml(self) -> AsyncAml: + from didit.resources.aml import AsyncAml + + return AsyncAml(self) + + @cached_property + def aml_monitoring(self) -> AsyncAmlMonitoring: + from didit.resources.aml_monitoring import AsyncAmlMonitoring + + return AsyncAmlMonitoring(self) + + @cached_property + def billing(self) -> AsyncBilling: + from didit.resources.billing import AsyncBilling + + return AsyncBilling(self) + + @cached_property + def biometric_templates(self) -> AsyncBiometricTemplates: + from didit.resources.biometric_templates import AsyncBiometricTemplates + + return AsyncBiometricTemplates(self) + + @cached_property + def business_sessions(self) -> AsyncBusinessSessions: + from didit.resources.business_sessions import AsyncBusinessSessions + + return AsyncBusinessSessions(self) + + @cached_property + def businesses(self) -> AsyncBusinesses: + from didit.resources.businesses import AsyncBusinesses + + return AsyncBusinesses(self) + + @cached_property + def cases(self) -> AsyncCases: + from didit.resources.cases import AsyncCases + + return AsyncCases(self) + + @cached_property + def customization(self) -> AsyncCustomization: + from didit.resources.customization import AsyncCustomization + + return AsyncCustomization(self) + + @cached_property + def database_validation(self) -> AsyncDatabaseValidation: + from didit.resources.database_validation import AsyncDatabaseValidation + + return AsyncDatabaseValidation(self) + + @cached_property + def document_ai(self) -> AsyncDocumentAi: + from didit.resources.document_ai import AsyncDocumentAi + + return AsyncDocumentAi(self) + + @cached_property + def email(self) -> AsyncEmail: + from didit.resources.email import AsyncEmail + + return AsyncEmail(self) + + @cached_property + def face_match(self) -> AsyncFaceMatch: + from didit.resources.face_match import AsyncFaceMatch + + return AsyncFaceMatch(self) + + @cached_property + def face_search(self) -> AsyncFaceSearch: + from didit.resources.face_search import AsyncFaceSearch + + return AsyncFaceSearch(self) + + @cached_property + def fraud_checks(self) -> AsyncFraudChecks: + from didit.resources.fraud_checks import AsyncFraudChecks + + return AsyncFraudChecks(self) + + @cached_property + def id_verification(self) -> AsyncIdVerification: + from didit.resources.id_verification import AsyncIdVerification + + return AsyncIdVerification(self) + + @cached_property + def id_wallets(self) -> AsyncIdWallets: + from didit.resources.id_wallets import AsyncIdWallets + + return AsyncIdWallets(self) + + @cached_property + def ip(self) -> AsyncIp: + from didit.resources.ip import AsyncIp + + return AsyncIp(self) + + @cached_property + def kyb(self) -> AsyncKyb: + from didit.resources.kyb import AsyncKyb + + return AsyncKyb(self) + + @cached_property + def networks(self) -> AsyncNetworks: + from didit.resources.networks import AsyncNetworks + + return AsyncNetworks(self) + + @cached_property + def passive_liveness(self) -> AsyncPassiveLiveness: + from didit.resources.passive_liveness import AsyncPassiveLiveness + + return AsyncPassiveLiveness(self) + + @cached_property + def phone(self) -> AsyncPhone: + from didit.resources.phone import AsyncPhone + + return AsyncPhone(self) + + @cached_property + def proof_of_address(self) -> AsyncProofOfAddress: + from didit.resources.proof_of_address import AsyncProofOfAddress + + return AsyncProofOfAddress(self) + + @cached_property + def questionnaires(self) -> AsyncQuestionnaires: + from didit.resources.questionnaires import AsyncQuestionnaires + + return AsyncQuestionnaires(self) + + @cached_property + def regulatory_reports(self) -> AsyncRegulatoryReports: + from didit.resources.regulatory_reports import AsyncRegulatoryReports + + return AsyncRegulatoryReports(self) + + @cached_property + def report_templates(self) -> AsyncReportTemplates: + from didit.resources.report_templates import AsyncReportTemplates + + return AsyncReportTemplates(self) + + @cached_property + def sessions(self) -> AsyncSessions: + from didit.resources.sessions import AsyncSessions + + return AsyncSessions(self) + + @cached_property + def transactions(self) -> AsyncTransactions: + from didit.resources.transactions import AsyncTransactions + + return AsyncTransactions(self) + + @cached_property + def travel_rule(self) -> AsyncTravelRule: + from didit.resources.travel_rule import AsyncTravelRule + + return AsyncTravelRule(self) + + @cached_property + def users(self) -> AsyncUsers: + from didit.resources.users import AsyncUsers + + return AsyncUsers(self) + + @cached_property + def wallet_screening(self) -> AsyncWalletScreening: + from didit.resources.wallet_screening import AsyncWalletScreening + + return AsyncWalletScreening(self) + + @cached_property + def webhooks(self) -> AsyncWebhooks: + from didit.resources.webhooks import AsyncWebhooks + + return AsyncWebhooks(self) + + @cached_property + def workflows(self) -> AsyncWorkflows: + from didit.resources.workflows import AsyncWorkflows + + return AsyncWorkflows(self) diff --git a/src/didit/resources/account/__init__.py b/src/didit/resources/account/__init__.py new file mode 100644 index 0000000..6d27a0e --- /dev/null +++ b/src/didit/resources/account/__init__.py @@ -0,0 +1,522 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``account`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.login_response import LoginResponse +from didit.types.components.register_response import RegisterResponse +from didit.types.components.verify_email_response import VerifyEmailResponse + +if TYPE_CHECKING: + from didit.resources.account.applications import ( + AccountApplications, + AccountApplicationsWithRawResponse, + AccountApplicationsWithStreamingResponse, + AsyncAccountApplications, + AsyncAccountApplicationsWithRawResponse, + AsyncAccountApplicationsWithStreamingResponse, + ) + from didit.resources.account.organizations import ( + AccountOrganizations, + AccountOrganizationsWithRawResponse, + AccountOrganizationsWithStreamingResponse, + AsyncAccountOrganizations, + AsyncAccountOrganizationsWithRawResponse, + AsyncAccountOrganizationsWithStreamingResponse, + ) + +__all__ = [ + "Account", + "AsyncAccount", + "AccountWithRawResponse", + "AsyncAccountWithRawResponse", + "AccountWithStreamingResponse", + "AsyncAccountWithStreamingResponse", +] + + +class Account(SyncAPIResource): + """The ``account`` resource of the Didit API. + + Methods: ``login``, ``register``, ``resend_otp``, ``verify_email``. + + Nested resources: ``applications``, ``organizations``. + """ + + @cached_property + def applications(self) -> AccountApplications: + from didit.resources.account.applications import AccountApplications + + return AccountApplications(self._client) + + @cached_property + def organizations(self) -> AccountOrganizations: + from didit.resources.account.organizations import AccountOrganizations + + return AccountOrganizations(self._client) + + @cached_property + def with_raw_response(self) -> AccountWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AccountWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccountWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AccountWithStreamingResponse(self) + + def register( + self, + *, + email: str, + password: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegisterResponse: + """Register Account. + + Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). Rate limit: 5/IP/hour. + + ``POST /programmatic/register/`` + + Args: + email: Any valid email address (personal, work, alias). Used as the unique account identifier. + password: Minimum 8 characters. Must contain at least one uppercase letter (`A-Z`), one lowercase letter (`a-z`), one digit (`0-9`), and one special character from `!@#$%^&*()_+-=[]{}|;:,.<>?`. Validation reports one failed rule at a time. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegisterResponse", + self._client.request( + "post", + "/programmatic/register/", + cast_to=RegisterResponse, + auth="none", + api="account", + body={"email": email, "password": password}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def verify_email( + self, + *, + email: str, + code: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> VerifyEmailResponse: + """Verify Email and Issue Credentials. + + Step 2 of onboarding. Exchanges the 6-char code for a verified account, a default org+app, and a JWT pair. Persist the returned `api_key`. + + ``POST /programmatic/verify-email/`` + + Args: + email: Email used during `POST /programmatic/register/`. + code: 6-character alphanumeric code from the verification email. Codes expire 10 minutes after the most recent code was sent. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "VerifyEmailResponse", + self._client.request( + "post", + "/programmatic/verify-email/", + cast_to=VerifyEmailResponse, + auth="none", + api="account", + body={"email": email, "code": code}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def login( + self, + *, + email: str, + password: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> LoginResponse: + """Programmatic Login. + + Authenticate any verified email/password Didit account and receive an RS256 JWT pair (default lifetime 86400s). Console-created email/password accounts can use this endpoint; OAuth-only console accounts must set a password first. + + ``POST /programmatic/login/`` + + Args: + email + password + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "LoginResponse", + self._client.request( + "post", + "/programmatic/login/", + cast_to=LoginResponse, + auth="none", + api="account", + body={"email": email, "password": password}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def resend_otp( + self, + *, + email: str, + password: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Resend the email verification code. + + Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes. + + Rate limits: a per-email cooldown of 60 seconds between resends (HTTP 429 with `Retry-After`), and the same 5-per-IP-per-hour budget as registration. + + ``POST /programmatic/resend-otp/`` + + Args: + email: Email of the pending (unverified) account. + password: Password chosen at registration. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + self._client.request( + "post", + "/programmatic/resend-otp/", + cast_to=object, + auth="none", + api="account", + body={"email": email, "password": password}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAccount(AsyncAPIResource): + """The ``account`` resource of the Didit API. + + Methods: ``login``, ``register``, ``resend_otp``, ``verify_email``. + + Nested resources: ``applications``, ``organizations``. + """ + + @cached_property + def applications(self) -> AsyncAccountApplications: + from didit.resources.account.applications import AsyncAccountApplications + + return AsyncAccountApplications(self._client) + + @cached_property + def organizations(self) -> AsyncAccountOrganizations: + from didit.resources.account.organizations import AsyncAccountOrganizations + + return AsyncAccountOrganizations(self._client) + + @cached_property + def with_raw_response(self) -> AsyncAccountWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAccountWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccountWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAccountWithStreamingResponse(self) + + async def register( + self, + *, + email: str, + password: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegisterResponse: + """Register Account. + + Step 1 of programmatic onboarding. Creates a pending account and emails a 6-char code (10 min TTL). Rate limit: 5/IP/hour. + + ``POST /programmatic/register/`` + + Args: + email: Any valid email address (personal, work, alias). Used as the unique account identifier. + password: Minimum 8 characters. Must contain at least one uppercase letter (`A-Z`), one lowercase letter (`a-z`), one digit (`0-9`), and one special character from `!@#$%^&*()_+-=[]{}|;:,.<>?`. Validation reports one failed rule at a time. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegisterResponse", + await self._client.request( + "post", + "/programmatic/register/", + cast_to=RegisterResponse, + auth="none", + api="account", + body={"email": email, "password": password}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def verify_email( + self, + *, + email: str, + code: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> VerifyEmailResponse: + """Verify Email and Issue Credentials. + + Step 2 of onboarding. Exchanges the 6-char code for a verified account, a default org+app, and a JWT pair. Persist the returned `api_key`. + + ``POST /programmatic/verify-email/`` + + Args: + email: Email used during `POST /programmatic/register/`. + code: 6-character alphanumeric code from the verification email. Codes expire 10 minutes after the most recent code was sent. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "VerifyEmailResponse", + await self._client.request( + "post", + "/programmatic/verify-email/", + cast_to=VerifyEmailResponse, + auth="none", + api="account", + body={"email": email, "code": code}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def login( + self, + *, + email: str, + password: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> LoginResponse: + """Programmatic Login. + + Authenticate any verified email/password Didit account and receive an RS256 JWT pair (default lifetime 86400s). Console-created email/password accounts can use this endpoint; OAuth-only console accounts must set a password first. + + ``POST /programmatic/login/`` + + Args: + email + password + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "LoginResponse", + await self._client.request( + "post", + "/programmatic/login/", + cast_to=LoginResponse, + auth="none", + api="account", + body={"email": email, "password": password}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def resend_otp( + self, + *, + email: str, + password: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Resend the email verification code. + + Resend the 6-character verification code for an unverified programmatic account. Requires the account's email and password so only the owner can trigger a new code (no console Basic Auth). A fresh code replaces the previous one and is valid for 10 minutes. + + Rate limits: a per-email cooldown of 60 seconds between resends (HTTP 429 with `Retry-After`), and the same 5-per-IP-per-hour budget as registration. + + ``POST /programmatic/resend-otp/`` + + Args: + email: Email of the pending (unverified) account. + password: Password chosen at registration. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + await self._client.request( + "post", + "/programmatic/resend-otp/", + cast_to=object, + auth="none", + api="account", + body={"email": email, "password": password}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AccountWithRawResponse: + def __init__(self, resource: Account) -> None: + self._resource = resource + self.register = to_raw_response_wrapper(resource.register) + self.verify_email = to_raw_response_wrapper(resource.verify_email) + self.login = to_raw_response_wrapper(resource.login) + self.resend_otp = to_raw_response_wrapper(resource.resend_otp) + + @cached_property + def applications(self) -> AccountApplicationsWithRawResponse: + from didit.resources.account.applications import AccountApplicationsWithRawResponse + + return AccountApplicationsWithRawResponse(self._resource.applications) + + @cached_property + def organizations(self) -> AccountOrganizationsWithRawResponse: + from didit.resources.account.organizations import AccountOrganizationsWithRawResponse + + return AccountOrganizationsWithRawResponse(self._resource.organizations) + + +class AsyncAccountWithRawResponse: + def __init__(self, resource: AsyncAccount) -> None: + self._resource = resource + self.register = async_to_raw_response_wrapper(resource.register) + self.verify_email = async_to_raw_response_wrapper(resource.verify_email) + self.login = async_to_raw_response_wrapper(resource.login) + self.resend_otp = async_to_raw_response_wrapper(resource.resend_otp) + + @cached_property + def applications(self) -> AsyncAccountApplicationsWithRawResponse: + from didit.resources.account.applications import AsyncAccountApplicationsWithRawResponse + + return AsyncAccountApplicationsWithRawResponse(self._resource.applications) + + @cached_property + def organizations(self) -> AsyncAccountOrganizationsWithRawResponse: + from didit.resources.account.organizations import AsyncAccountOrganizationsWithRawResponse + + return AsyncAccountOrganizationsWithRawResponse(self._resource.organizations) + + +class AccountWithStreamingResponse: + def __init__(self, resource: Account) -> None: + self._resource = resource + self.register = to_streamed_response_wrapper(resource.register) + self.verify_email = to_streamed_response_wrapper(resource.verify_email) + self.login = to_streamed_response_wrapper(resource.login) + self.resend_otp = to_streamed_response_wrapper(resource.resend_otp) + + @cached_property + def applications(self) -> AccountApplicationsWithStreamingResponse: + from didit.resources.account.applications import AccountApplicationsWithStreamingResponse + + return AccountApplicationsWithStreamingResponse(self._resource.applications) + + @cached_property + def organizations(self) -> AccountOrganizationsWithStreamingResponse: + from didit.resources.account.organizations import AccountOrganizationsWithStreamingResponse + + return AccountOrganizationsWithStreamingResponse(self._resource.organizations) + + +class AsyncAccountWithStreamingResponse: + def __init__(self, resource: AsyncAccount) -> None: + self._resource = resource + self.register = async_to_streamed_response_wrapper(resource.register) + self.verify_email = async_to_streamed_response_wrapper(resource.verify_email) + self.login = async_to_streamed_response_wrapper(resource.login) + self.resend_otp = async_to_streamed_response_wrapper(resource.resend_otp) + + @cached_property + def applications(self) -> AsyncAccountApplicationsWithStreamingResponse: + from didit.resources.account.applications import ( + AsyncAccountApplicationsWithStreamingResponse, + ) + + return AsyncAccountApplicationsWithStreamingResponse(self._resource.applications) + + @cached_property + def organizations(self) -> AsyncAccountOrganizationsWithStreamingResponse: + from didit.resources.account.organizations import ( + AsyncAccountOrganizationsWithStreamingResponse, + ) + + return AsyncAccountOrganizationsWithStreamingResponse(self._resource.organizations) diff --git a/src/didit/resources/account/applications.py b/src/didit/resources/account/applications.py new file mode 100644 index 0000000..2c85062 --- /dev/null +++ b/src/didit/resources/account/applications.py @@ -0,0 +1,490 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``account.applications`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Sequence, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.application import Application + +__all__ = [ + "AccountApplications", + "AsyncAccountApplications", + "AccountApplicationsWithRawResponse", + "AsyncAccountApplicationsWithRawResponse", + "AccountApplicationsWithStreamingResponse", + "AsyncAccountApplicationsWithStreamingResponse", +] + + +class AccountApplications(SyncAPIResource): + """The ``account.applications`` resource of the Didit API. + + Methods: ``create``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AccountApplicationsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AccountApplicationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccountApplicationsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AccountApplicationsWithStreamingResponse(self) + + def list( + self, + org_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """List the organization's applications. + + List every application in the organization. Use this to discover `app_id` values when recovering credentials from a new machine - follow with `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token. + + ``GET /organizations/me/{org_id}/applications/`` + + Args: + org_id: UUID of the organization. Look it up with `GET /organizations/me/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + self._client.request( + "get", + "/organizations/me/{org_id}/applications/", + cast_to=object, + path_params={"org_id": org_id}, + auth="account_token", + api="account", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + org_id: str, + *, + name: str | NotGiven = NOT_GIVEN, + website_url: str | NotGiven = NOT_GIVEN, + redirect_uris: Sequence[str] | NotGiven = NOT_GIVEN, + terms_url: str | NotGiven = NOT_GIVEN, + privacy_url: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Application: + """Create Application. + + Create an application inside an organization. The response includes `api_key`; persist it now (recoverable via GET). Requires owner/admin JWT. + + ``POST /organizations/me/{org_id}/applications/`` + + Args: + org_id: UUID of the organization. Look it up with `GET /organizations/me/`. + name: Application display name. Defaults to `" App"`. + website_url: Website or app URL associated with this application. + redirect_uris: Allowed redirect URIs for OAuth-style and verification redirect flows. + terms_url: Terms of service URL shown in the verification flow. + privacy_url: Privacy policy URL shown in the verification flow. + description: Internal description for the application (not shown to end users). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "Application", + self._client.request( + "post", + "/organizations/me/{org_id}/applications/", + cast_to=Application, + path_params={"org_id": org_id}, + auth="account_token", + api="account", + body={ + "name": name, + "website_url": website_url, + "redirect_uris": redirect_uris, + "terms_url": terms_url, + "privacy_url": privacy_url, + "description": description, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + org_id: str, + app_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Application: + """Get Application Credentials. + + Read one application's full record including `client_id` and `api_key`. Use this to recover a lost `api_key`. + + ``GET /organizations/me/{org_id}/applications/{app_id}/`` + + Args: + org_id: UUID of the organization that owns the application. + app_id: UUID of the application whose credentials you want to read. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "Application", + self._client.request( + "get", + "/organizations/me/{org_id}/applications/{app_id}/", + cast_to=Application, + path_params={"org_id": org_id, "app_id": app_id}, + auth="account_token", + api="account", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + org_id: str, + app_id: str, + *, + name: str | NotGiven = NOT_GIVEN, + website_url: str | NotGiven = NOT_GIVEN, + redirect_uris: Sequence[str] | NotGiven = NOT_GIVEN, + terms_url: str | NotGiven = NOT_GIVEN, + privacy_url: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Application: + """Update Application. + + Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. Requires owner/admin JWT. + + ``PATCH /organizations/me/{org_id}/applications/{app_id}/`` + + Args: + org_id: UUID of the organization that owns the application. + app_id: UUID of the application to update. + name: Application display name. + website_url: Website or app URL associated with this application. + redirect_uris: Allowed redirect URIs for OAuth-style and verification redirect flows. + terms_url: Terms of service URL shown in the verification flow. + privacy_url: Privacy policy URL shown in the verification flow. + description: Internal description for the application. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "Application", + self._client.request( + "patch", + "/organizations/me/{org_id}/applications/{app_id}/", + cast_to=Application, + path_params={"org_id": org_id, "app_id": app_id}, + auth="account_token", + api="account", + body={ + "name": name, + "website_url": website_url, + "redirect_uris": redirect_uris, + "terms_url": terms_url, + "privacy_url": privacy_url, + "description": description, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAccountApplications(AsyncAPIResource): + """The ``account.applications`` resource of the Didit API. + + Methods: ``create``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncAccountApplicationsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAccountApplicationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccountApplicationsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAccountApplicationsWithStreamingResponse(self) + + async def list( + self, + org_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """List the organization's applications. + + List every application in the organization. Use this to discover `app_id` values when recovering credentials from a new machine - follow with `GET /organizations/me/{org_id}/applications/{app_id}/` for the API key. Requires an owner or admin Bearer token. + + ``GET /organizations/me/{org_id}/applications/`` + + Args: + org_id: UUID of the organization. Look it up with `GET /organizations/me/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + await self._client.request( + "get", + "/organizations/me/{org_id}/applications/", + cast_to=object, + path_params={"org_id": org_id}, + auth="account_token", + api="account", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + org_id: str, + *, + name: str | NotGiven = NOT_GIVEN, + website_url: str | NotGiven = NOT_GIVEN, + redirect_uris: Sequence[str] | NotGiven = NOT_GIVEN, + terms_url: str | NotGiven = NOT_GIVEN, + privacy_url: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Application: + """Create Application. + + Create an application inside an organization. The response includes `api_key`; persist it now (recoverable via GET). Requires owner/admin JWT. + + ``POST /organizations/me/{org_id}/applications/`` + + Args: + org_id: UUID of the organization. Look it up with `GET /organizations/me/`. + name: Application display name. Defaults to `" App"`. + website_url: Website or app URL associated with this application. + redirect_uris: Allowed redirect URIs for OAuth-style and verification redirect flows. + terms_url: Terms of service URL shown in the verification flow. + privacy_url: Privacy policy URL shown in the verification flow. + description: Internal description for the application (not shown to end users). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "Application", + await self._client.request( + "post", + "/organizations/me/{org_id}/applications/", + cast_to=Application, + path_params={"org_id": org_id}, + auth="account_token", + api="account", + body={ + "name": name, + "website_url": website_url, + "redirect_uris": redirect_uris, + "terms_url": terms_url, + "privacy_url": privacy_url, + "description": description, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + org_id: str, + app_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Application: + """Get Application Credentials. + + Read one application's full record including `client_id` and `api_key`. Use this to recover a lost `api_key`. + + ``GET /organizations/me/{org_id}/applications/{app_id}/`` + + Args: + org_id: UUID of the organization that owns the application. + app_id: UUID of the application whose credentials you want to read. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "Application", + await self._client.request( + "get", + "/organizations/me/{org_id}/applications/{app_id}/", + cast_to=Application, + path_params={"org_id": org_id, "app_id": app_id}, + auth="account_token", + api="account", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + org_id: str, + app_id: str, + *, + name: str | NotGiven = NOT_GIVEN, + website_url: str | NotGiven = NOT_GIVEN, + redirect_uris: Sequence[str] | NotGiven = NOT_GIVEN, + terms_url: str | NotGiven = NOT_GIVEN, + privacy_url: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> Application: + """Update Application. + + Update application metadata (name, URLs, redirect URIs). `api_key` and `client_id` are never rotated. Requires owner/admin JWT. + + ``PATCH /organizations/me/{org_id}/applications/{app_id}/`` + + Args: + org_id: UUID of the organization that owns the application. + app_id: UUID of the application to update. + name: Application display name. + website_url: Website or app URL associated with this application. + redirect_uris: Allowed redirect URIs for OAuth-style and verification redirect flows. + terms_url: Terms of service URL shown in the verification flow. + privacy_url: Privacy policy URL shown in the verification flow. + description: Internal description for the application. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "Application", + await self._client.request( + "patch", + "/organizations/me/{org_id}/applications/{app_id}/", + cast_to=Application, + path_params={"org_id": org_id, "app_id": app_id}, + auth="account_token", + api="account", + body={ + "name": name, + "website_url": website_url, + "redirect_uris": redirect_uris, + "terms_url": terms_url, + "privacy_url": privacy_url, + "description": description, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AccountApplicationsWithRawResponse: + def __init__(self, resource: AccountApplications) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncAccountApplicationsWithRawResponse: + def __init__(self, resource: AsyncAccountApplications) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.update = async_to_raw_response_wrapper(resource.update) + + +class AccountApplicationsWithStreamingResponse: + def __init__(self, resource: AccountApplications) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncAccountApplicationsWithStreamingResponse: + def __init__(self, resource: AsyncAccountApplications) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/account/organizations.py b/src/didit/resources/account/organizations.py new file mode 100644 index 0000000..8582164 --- /dev/null +++ b/src/didit/resources/account/organizations.py @@ -0,0 +1,157 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``account.organizations`` resource.""" + +from __future__ import annotations + +import builtins +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.organization import Organization + +__all__ = [ + "AccountOrganizations", + "AsyncAccountOrganizations", + "AccountOrganizationsWithRawResponse", + "AsyncAccountOrganizationsWithRawResponse", + "AccountOrganizationsWithStreamingResponse", + "AsyncAccountOrganizationsWithStreamingResponse", +] + + +class AccountOrganizations(SyncAPIResource): + """The ``account.organizations`` resource of the Didit API. + + Methods: ``list``. + """ + + @cached_property + def with_raw_response(self) -> AccountOrganizationsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AccountOrganizationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AccountOrganizationsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AccountOrganizationsWithStreamingResponse(self) + + def list( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[Organization]: + """List My Organizations. + + List organizations the authenticated user belongs to. Each entry's `uuid` is the `org_id` used on the application endpoints. + + ``GET /organizations/me/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[Organization]", + self._client.request( + "get", + "/organizations/me/", + cast_to=builtins.list[Organization], + auth="account_token", + api="account", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAccountOrganizations(AsyncAPIResource): + """The ``account.organizations`` resource of the Didit API. + + Methods: ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncAccountOrganizationsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAccountOrganizationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAccountOrganizationsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAccountOrganizationsWithStreamingResponse(self) + + async def list( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[Organization]: + """List My Organizations. + + List organizations the authenticated user belongs to. Each entry's `uuid` is the `org_id` used on the application endpoints. + + ``GET /organizations/me/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[Organization]", + await self._client.request( + "get", + "/organizations/me/", + cast_to=builtins.list[Organization], + auth="account_token", + api="account", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AccountOrganizationsWithRawResponse: + def __init__(self, resource: AccountOrganizations) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + + +class AsyncAccountOrganizationsWithRawResponse: + def __init__(self, resource: AsyncAccountOrganizations) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + + +class AccountOrganizationsWithStreamingResponse: + def __init__(self, resource: AccountOrganizations) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + + +class AsyncAccountOrganizationsWithStreamingResponse: + def __init__(self, resource: AsyncAccountOrganizations) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) diff --git a/src/didit/resources/age_estimation.py b/src/didit/resources/age_estimation.py new file mode 100644 index 0000000..b0c35de --- /dev/null +++ b/src/didit/resources/age_estimation.py @@ -0,0 +1,232 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``age_estimation`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.age_estimation.estimate_response import AgeEstimationEstimateResponse + +__all__ = [ + "AgeEstimation", + "AsyncAgeEstimation", + "AgeEstimationWithRawResponse", + "AsyncAgeEstimationWithRawResponse", + "AgeEstimationWithStreamingResponse", + "AsyncAgeEstimationWithStreamingResponse", +] + + +class AgeEstimation(SyncAPIResource): + """The ``age_estimation`` resource of the Didit API. + + Methods: ``estimate``. + """ + + @cached_property + def with_raw_response(self) -> AgeEstimationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AgeEstimationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AgeEstimationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AgeEstimationWithStreamingResponse(self) + + def estimate( + self, + *, + user_image: FileTypes, + face_liveness_score_decline_threshold: float | NotGiven = NOT_GIVEN, + age_estimation_decline_threshold: int | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AgeEstimationEstimateResponse: + """Age Estimation (face age + passive liveness). + + Estimate a person's age from a single face photo and run a passive liveness check in the same call - returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** - use it for age gates and low-friction checks, not as legal proof of age. + + **How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results. + + **Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification - the model's error grows for faces close to the limit. + + **Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: ["AGE_ESTIMATION"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/age-estimation/`` + + Args: + user_image: Face photo to analyze (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp` (no PDF). Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.3 MB before processing, so very high resolutions do not improve accuracy. + face_liveness_score_decline_threshold: Liveness threshold (0–100). A liveness `score` **at or below** this value adds a `LOW_LIVENESS_SCORE` warning and declines. Raise it for stricter anti-spoofing; values outside 0–100 return `400`. + age_estimation_decline_threshold: Minimum acceptable estimated age in years. An estimated age **below** this value adds an `AGE_BELOW_MINIMUM` warning and declines. Set `0` to disable the age check (the call then only runs liveness + age estimation without an age-based decision). + rotate_image: When `true`, the service tries the input image in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection - useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images. + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the detected face added to your application's face index. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"age-gate"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AgeEstimationEstimateResponse", + self._client.request( + "post", + "/v3/age-estimation/", + cast_to=AgeEstimationEstimateResponse, + auth="api_key", + body={ + "user_image": user_image, + "face_liveness_score_decline_threshold": face_liveness_score_decline_threshold, + "age_estimation_decline_threshold": age_estimation_decline_threshold, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("user_image",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAgeEstimation(AsyncAPIResource): + """The ``age_estimation`` resource of the Didit API. + + Methods: ``estimate``. + """ + + @cached_property + def with_raw_response(self) -> AsyncAgeEstimationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAgeEstimationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAgeEstimationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAgeEstimationWithStreamingResponse(self) + + async def estimate( + self, + *, + user_image: FileTypes, + face_liveness_score_decline_threshold: float | NotGiven = NOT_GIVEN, + age_estimation_decline_threshold: int | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AgeEstimationEstimateResponse: + """Age Estimation (face age + passive liveness). + + Estimate a person's age from a single face photo and run a passive liveness check in the same call - returns the model-predicted `age_estimation` (in years), a liveness `score` (0–100), and an `Approved`/`Declined` `status`. The age is **model-predicted from the face, not verified against a document** - use it for age gates and low-friction checks, not as legal proof of age. + + **How it works.** The image is analyzed for faces (set `rotate_image=true` if captures may be sideways or upside down); each detected face gets an estimated age, and the largest face in the frame drives `age_estimation`. In parallel, a passive liveness model scores whether the photo shows a live person (no user challenge required) and detects presentation attacks (photos of photos, screens, masks). Submit one clear, front-facing, well-lit face per image for best results. + + **Decision logic.** Any warning declines: `status` is `Approved` only when the `warnings` array is empty. A liveness `score` at or below `face_liveness_score_decline_threshold` (default `30`) adds `LOW_LIVENESS_SCORE`; a detected attack adds `LIVENESS_FACE_ATTACK`; no face found by the liveness model adds `NO_FACE_DETECTED`; no estimable face age adds `AGE_NOT_DETECTED` (`age_estimation` is `null`); an estimated age below `age_estimation_decline_threshold` (default `18`) adds `AGE_BELOW_MINIMUM`. Borderline ages near your threshold deserve a fallback to document-based verification - the model's error grows for faces close to the limit. + + **Billing.** Each `200` response consumes one Age Estimation API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the result is exposed there in `liveness_checks[]`, with `features: ["AGE_ESTIMATION"]`), the face crop and reference image are stored, the face embedding is added to your application's face index (used by face search and duplicate detection), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (`age_estimation: 30`, no warnings), no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/age-estimation/`` + + Args: + user_image: Face photo to analyze (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp` (no PDF). Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.3 MB before processing, so very high resolutions do not improve accuracy. + face_liveness_score_decline_threshold: Liveness threshold (0–100). A liveness `score` **at or below** this value adds a `LOW_LIVENESS_SCORE` warning and declines. Raise it for stricter anti-spoofing; values outside 0–100 return `400`. + age_estimation_decline_threshold: Minimum acceptable estimated age in years. An estimated age **below** this value adds an `AGE_BELOW_MINIMUM` warning and declines. Set `0` to disable the age check (the call then only runs liveness + age estimation without an age-based decision). + rotate_image: When `true`, the service tries the input image in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection - useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images. + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the detected face added to your application's face index. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"age-gate"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AgeEstimationEstimateResponse", + await self._client.request( + "post", + "/v3/age-estimation/", + cast_to=AgeEstimationEstimateResponse, + auth="api_key", + body={ + "user_image": user_image, + "face_liveness_score_decline_threshold": face_liveness_score_decline_threshold, + "age_estimation_decline_threshold": age_estimation_decline_threshold, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("user_image",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AgeEstimationWithRawResponse: + def __init__(self, resource: AgeEstimation) -> None: + self._resource = resource + self.estimate = to_raw_response_wrapper(resource.estimate) + + +class AsyncAgeEstimationWithRawResponse: + def __init__(self, resource: AsyncAgeEstimation) -> None: + self._resource = resource + self.estimate = async_to_raw_response_wrapper(resource.estimate) + + +class AgeEstimationWithStreamingResponse: + def __init__(self, resource: AgeEstimation) -> None: + self._resource = resource + self.estimate = to_streamed_response_wrapper(resource.estimate) + + +class AsyncAgeEstimationWithStreamingResponse: + def __init__(self, resource: AsyncAgeEstimation) -> None: + self._resource = resource + self.estimate = async_to_streamed_response_wrapper(resource.estimate) diff --git a/src/didit/resources/aml.py b/src/didit/resources/aml.py new file mode 100644 index 0000000..e95e515 --- /dev/null +++ b/src/didit/resources/aml.py @@ -0,0 +1,295 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``aml`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.aml.screen_response import AmlScreenResponse + +__all__ = [ + "Aml", + "AsyncAml", + "AmlWithRawResponse", + "AsyncAmlWithRawResponse", + "AmlWithStreamingResponse", + "AsyncAmlWithStreamingResponse", +] + + +class Aml(SyncAPIResource): + """The ``aml`` resource of the Didit API. + + Methods: ``screen``. + """ + + @cached_property + def with_raw_response(self) -> AmlWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AmlWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AmlWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AmlWithStreamingResponse(self) + + def screen( + self, + *, + full_name: str, + entity_type: Literal["person", "company"] | str | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | NotGiven = NOT_GIVEN, + nationality: str | NotGiven = NOT_GIVEN, + document_number: str | NotGiven = NOT_GIVEN, + aml_score_approve_threshold: float | NotGiven = NOT_GIVEN, + aml_score_review_threshold: float | NotGiven = NOT_GIVEN, + aml_name_weight: int | NotGiven = NOT_GIVEN, + aml_dob_weight: int | NotGiven = NOT_GIVEN, + aml_country_weight: int | NotGiven = NOT_GIVEN, + aml_match_score_threshold: int | NotGiven = NOT_GIVEN, + include_adverse_media: bool | NotGiven = NOT_GIVEN, + include_ongoing_monitoring: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AmlScreenResponse: + """AML screening (standalone). + + Screen a person or company against PEP, sanctions, warning/watchlist, and adverse-media datasets in one call. + + **Two-score model.** Each hit carries a `match_score` (0–100, identity confidence) and a `risk_score` (0–100, entity risk). Hits whose `match_score` is below `aml_match_score_threshold` (default **93**) are tagged `review_status: "False Positive"` and ignored for risk purposes; the highest `risk_score` among the remaining hits becomes `aml.score`. The final `aml.status` follows your thresholds: `score` > `aml_score_review_threshold` (default **100**) → `Declined`; `score` > `aml_score_approve_threshold` (default **80**) → `In Review`; otherwise `Approved`. Whenever `score` is non-zero and ≥ the approve threshold a `POSSIBLE_MATCH_FOUND` warning is added (a score of exactly 0 never warns). + + **Match-score weights.** `aml_name_weight` + `aml_dob_weight` + `aml_country_weight` must sum to exactly 100 (defaults 60/25/15). Components missing from the request are re-normalized away; a matching `document_number` acts as a golden key that overrides `match_score` to 100. + + **Latency.** Plain screenings typically complete in a few seconds. With `include_adverse_media=true` the API waits for adverse-media enrichment (an initial ~5 s pause plus up to ~25 s of polling) - configure a client timeout of **at least 60 seconds**. + + **Persistence.** `save_api_request` defaults to **true**: the screening is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), it appears in the console, a `status.updated` webhook fires, and hit review statuses can be updated later. With `save_api_request=false` nothing is stored and `request_id` is a transient correlation id. `include_ongoing_monitoring=true` keeps re-screening the profile and requires `save_api_request=true` (enforced with a `400`). + + **Idempotency.** Send an `Idempotency-Key` header when retrying after a timeout or transient error. The key is scoped to your application and the exact request body. The first completed request stores its response; later identical retries return that same response and do not run or bill another AML screening. If the original request is still running, retry later with the same key. If the same key is reused with a different body, the API returns `409`. + + **Billing.** One AML API credit is charged after the first successful screening. Replayed idempotent responses are not charged again; an insufficient balance returns `403` before any new screening happens. + + **Sandbox.** Keys from sandbox applications skip screening and billing entirely and return a mocked result - `Approved` with zero hits unless you pass `sandbox_scenario`. `save_api_request` works the same as on live keys, so a saved sandbox screening is retrievable through `GET /v3/session/{sessionId}/decision/` and `GET /v3/session/{sessionId}/generate-pdf/`. + + ``POST /v3/aml/`` + + Args: + full_name: Full name of the person or company to screen. Minimum **2 characters** - the screening provider's own floor. A shorter or blank name returns `400` on this field before any screening runs, so nothing is billed. + entity_type: Type of entity to screen. Defaults to `person`. + date_of_birth: Date of birth (persons) or incorporation date (companies), `YYYY-MM-DD`. Improves match precision via the DOB weight. + nationality: ISO 3166-1 **alpha-2** country code (e.g. `ES`). Invalid codes return `400`. Improves match precision via the country weight. + document_number: Identity document number. Not weighted - acts as a golden key: an exact match overrides the hit's `match_score` to 100, a hard mismatch penalizes it. Sent to the screening provider as a search filter only when it is **2-50 characters**; a value outside that range is accepted and stored, and the screening still runs and is billed, just not narrowed by document number. + aml_score_approve_threshold: Risk-score threshold at or below which the result is `Approved`. Must be ≤ `aml_score_review_threshold` (else `400`). + aml_score_review_threshold: Risk-score threshold above which the result is `Declined`. Scores between the two thresholds produce `In Review`. + aml_name_weight: Weight of name similarity in the per-hit match score. The three weights must sum to exactly 100 (else `400`). + aml_dob_weight: Weight of date of birth in the per-hit match score. + aml_country_weight: Weight of country/nationality in the per-hit match score. + aml_match_score_threshold: Per-hit cutoff: hits with `match_score` below this are `False Positive` (excluded from risk assessment); at or above are `Unreviewed` possible matches. + include_adverse_media: Also search news media for negative coverage. Adds up to ~30 s of latency while results are gathered - use a ≥60 s client timeout. + include_ongoing_monitoring: Keep re-screening this profile and push changes via webhook. Requires `save_api_request=true` (else `400`). + save_api_request: Persist the screening as a session (console visibility, decision endpoint, webhooks, hit review). Set to `false` for a stateless check. + vendor_data: Your identifier for the screened user; echoed back and stored with the session. + metadata: Free-form JSON stored with the request and echoed back. + sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. A standalone screening has no session to arm, so the slug goes on the screening request itself: `decline_aml_hit` returns one production-shaped hit and settles the screening as `Declined`, `review_aml_possible_match` settles it `In Review`, and a scenario that arms no AML outcome leaves the screening clean. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AmlScreenResponse", + self._client.request( + "post", + "/v3/aml/", + cast_to=AmlScreenResponse, + auth="api_key", + body={ + "full_name": full_name, + "entity_type": entity_type, + "date_of_birth": date_of_birth, + "nationality": nationality, + "document_number": document_number, + "aml_score_approve_threshold": aml_score_approve_threshold, + "aml_score_review_threshold": aml_score_review_threshold, + "aml_name_weight": aml_name_weight, + "aml_dob_weight": aml_dob_weight, + "aml_country_weight": aml_country_weight, + "aml_match_score_threshold": aml_match_score_threshold, + "include_adverse_media": include_adverse_media, + "include_ongoing_monitoring": include_ongoing_monitoring, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAml(AsyncAPIResource): + """The ``aml`` resource of the Didit API. + + Methods: ``screen``. + """ + + @cached_property + def with_raw_response(self) -> AsyncAmlWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAmlWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAmlWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAmlWithStreamingResponse(self) + + async def screen( + self, + *, + full_name: str, + entity_type: Literal["person", "company"] | str | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | NotGiven = NOT_GIVEN, + nationality: str | NotGiven = NOT_GIVEN, + document_number: str | NotGiven = NOT_GIVEN, + aml_score_approve_threshold: float | NotGiven = NOT_GIVEN, + aml_score_review_threshold: float | NotGiven = NOT_GIVEN, + aml_name_weight: int | NotGiven = NOT_GIVEN, + aml_dob_weight: int | NotGiven = NOT_GIVEN, + aml_country_weight: int | NotGiven = NOT_GIVEN, + aml_match_score_threshold: int | NotGiven = NOT_GIVEN, + include_adverse_media: bool | NotGiven = NOT_GIVEN, + include_ongoing_monitoring: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AmlScreenResponse: + """AML screening (standalone). + + Screen a person or company against PEP, sanctions, warning/watchlist, and adverse-media datasets in one call. + + **Two-score model.** Each hit carries a `match_score` (0–100, identity confidence) and a `risk_score` (0–100, entity risk). Hits whose `match_score` is below `aml_match_score_threshold` (default **93**) are tagged `review_status: "False Positive"` and ignored for risk purposes; the highest `risk_score` among the remaining hits becomes `aml.score`. The final `aml.status` follows your thresholds: `score` > `aml_score_review_threshold` (default **100**) → `Declined`; `score` > `aml_score_approve_threshold` (default **80**) → `In Review`; otherwise `Approved`. Whenever `score` is non-zero and ≥ the approve threshold a `POSSIBLE_MATCH_FOUND` warning is added (a score of exactly 0 never warns). + + **Match-score weights.** `aml_name_weight` + `aml_dob_weight` + `aml_country_weight` must sum to exactly 100 (defaults 60/25/15). Components missing from the request are re-normalized away; a matching `document_number` acts as a golden key that overrides `match_score` to 100. + + **Latency.** Plain screenings typically complete in a few seconds. With `include_adverse_media=true` the API waits for adverse-media enrichment (an initial ~5 s pause plus up to ~25 s of polling) - configure a client timeout of **at least 60 seconds**. + + **Persistence.** `save_api_request` defaults to **true**: the screening is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), it appears in the console, a `status.updated` webhook fires, and hit review statuses can be updated later. With `save_api_request=false` nothing is stored and `request_id` is a transient correlation id. `include_ongoing_monitoring=true` keeps re-screening the profile and requires `save_api_request=true` (enforced with a `400`). + + **Idempotency.** Send an `Idempotency-Key` header when retrying after a timeout or transient error. The key is scoped to your application and the exact request body. The first completed request stores its response; later identical retries return that same response and do not run or bill another AML screening. If the original request is still running, retry later with the same key. If the same key is reused with a different body, the API returns `409`. + + **Billing.** One AML API credit is charged after the first successful screening. Replayed idempotent responses are not charged again; an insufficient balance returns `403` before any new screening happens. + + **Sandbox.** Keys from sandbox applications skip screening and billing entirely and return a mocked result - `Approved` with zero hits unless you pass `sandbox_scenario`. `save_api_request` works the same as on live keys, so a saved sandbox screening is retrievable through `GET /v3/session/{sessionId}/decision/` and `GET /v3/session/{sessionId}/generate-pdf/`. + + ``POST /v3/aml/`` + + Args: + full_name: Full name of the person or company to screen. Minimum **2 characters** - the screening provider's own floor. A shorter or blank name returns `400` on this field before any screening runs, so nothing is billed. + entity_type: Type of entity to screen. Defaults to `person`. + date_of_birth: Date of birth (persons) or incorporation date (companies), `YYYY-MM-DD`. Improves match precision via the DOB weight. + nationality: ISO 3166-1 **alpha-2** country code (e.g. `ES`). Invalid codes return `400`. Improves match precision via the country weight. + document_number: Identity document number. Not weighted - acts as a golden key: an exact match overrides the hit's `match_score` to 100, a hard mismatch penalizes it. Sent to the screening provider as a search filter only when it is **2-50 characters**; a value outside that range is accepted and stored, and the screening still runs and is billed, just not narrowed by document number. + aml_score_approve_threshold: Risk-score threshold at or below which the result is `Approved`. Must be ≤ `aml_score_review_threshold` (else `400`). + aml_score_review_threshold: Risk-score threshold above which the result is `Declined`. Scores between the two thresholds produce `In Review`. + aml_name_weight: Weight of name similarity in the per-hit match score. The three weights must sum to exactly 100 (else `400`). + aml_dob_weight: Weight of date of birth in the per-hit match score. + aml_country_weight: Weight of country/nationality in the per-hit match score. + aml_match_score_threshold: Per-hit cutoff: hits with `match_score` below this are `False Positive` (excluded from risk assessment); at or above are `Unreviewed` possible matches. + include_adverse_media: Also search news media for negative coverage. Adds up to ~30 s of latency while results are gathered - use a ≥60 s client timeout. + include_ongoing_monitoring: Keep re-screening this profile and push changes via webhook. Requires `save_api_request=true` (else `400`). + save_api_request: Persist the screening as a session (console visibility, decision endpoint, webhooks, hit review). Set to `false` for a stateless check. + vendor_data: Your identifier for the screened user; echoed back and stored with the session. + metadata: Free-form JSON stored with the request and echoed back. + sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. A standalone screening has no session to arm, so the slug goes on the screening request itself: `decline_aml_hit` returns one production-shaped hit and settles the screening as `Declined`, `review_aml_possible_match` settles it `In Review`, and a scenario that arms no AML outcome leaves the screening clean. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AmlScreenResponse", + await self._client.request( + "post", + "/v3/aml/", + cast_to=AmlScreenResponse, + auth="api_key", + body={ + "full_name": full_name, + "entity_type": entity_type, + "date_of_birth": date_of_birth, + "nationality": nationality, + "document_number": document_number, + "aml_score_approve_threshold": aml_score_approve_threshold, + "aml_score_review_threshold": aml_score_review_threshold, + "aml_name_weight": aml_name_weight, + "aml_dob_weight": aml_dob_weight, + "aml_country_weight": aml_country_weight, + "aml_match_score_threshold": aml_match_score_threshold, + "include_adverse_media": include_adverse_media, + "include_ongoing_monitoring": include_ongoing_monitoring, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AmlWithRawResponse: + def __init__(self, resource: Aml) -> None: + self._resource = resource + self.screen = to_raw_response_wrapper(resource.screen) + + +class AsyncAmlWithRawResponse: + def __init__(self, resource: AsyncAml) -> None: + self._resource = resource + self.screen = async_to_raw_response_wrapper(resource.screen) + + +class AmlWithStreamingResponse: + def __init__(self, resource: Aml) -> None: + self._resource = resource + self.screen = to_streamed_response_wrapper(resource.screen) + + +class AsyncAmlWithStreamingResponse: + def __init__(self, resource: AsyncAml) -> None: + self._resource = resource + self.screen = async_to_streamed_response_wrapper(resource.screen) diff --git a/src/didit/resources/aml_monitoring.py b/src/didit/resources/aml_monitoring.py new file mode 100644 index 0000000..5ef18b5 --- /dev/null +++ b/src/didit/resources/aml_monitoring.py @@ -0,0 +1,160 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``aml_monitoring`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.aml_monitoring.retrieve_job_response import AmlMonitoringRetrieveJobResponse + +__all__ = [ + "AmlMonitoring", + "AsyncAmlMonitoring", + "AmlMonitoringWithRawResponse", + "AsyncAmlMonitoringWithRawResponse", + "AmlMonitoringWithStreamingResponse", + "AsyncAmlMonitoringWithStreamingResponse", +] + + +class AmlMonitoring(SyncAPIResource): + """The ``aml_monitoring`` resource of the Didit API. + + Methods: ``retrieve_job``. + """ + + @cached_property + def with_raw_response(self) -> AmlMonitoringWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AmlMonitoringWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AmlMonitoringWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AmlMonitoringWithStreamingResponse(self) + + def retrieve_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AmlMonitoringRetrieveJobResponse: + """Get AML ongoing monitoring job. + + Status, outcome counters and per-record results of a monitoring toggle job started from `POST /v3/users/aml-monitoring/` or `POST /v3/businesses/aml-monitoring/`. Poll every couple of seconds while `status` is `PENDING` or `PROCESSING`; `results` is complete once it is `COMPLETED`. + + ``GET /v3/aml-monitoring/jobs/{job_id}/`` + + Args: + job_id: The `job_id` returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AmlMonitoringRetrieveJobResponse", + self._client.request( + "get", + "/v3/aml-monitoring/jobs/{job_id}/", + cast_to=AmlMonitoringRetrieveJobResponse, + path_params={"job_id": job_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncAmlMonitoring(AsyncAPIResource): + """The ``aml_monitoring`` resource of the Didit API. + + Methods: ``retrieve_job``. + """ + + @cached_property + def with_raw_response(self) -> AsyncAmlMonitoringWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncAmlMonitoringWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncAmlMonitoringWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncAmlMonitoringWithStreamingResponse(self) + + async def retrieve_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AmlMonitoringRetrieveJobResponse: + """Get AML ongoing monitoring job. + + Status, outcome counters and per-record results of a monitoring toggle job started from `POST /v3/users/aml-monitoring/` or `POST /v3/businesses/aml-monitoring/`. Poll every couple of seconds while `status` is `PENDING` or `PROCESSING`; `results` is complete once it is `COMPLETED`. + + ``GET /v3/aml-monitoring/jobs/{job_id}/`` + + Args: + job_id: The `job_id` returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AmlMonitoringRetrieveJobResponse", + await self._client.request( + "get", + "/v3/aml-monitoring/jobs/{job_id}/", + cast_to=AmlMonitoringRetrieveJobResponse, + path_params={"job_id": job_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AmlMonitoringWithRawResponse: + def __init__(self, resource: AmlMonitoring) -> None: + self._resource = resource + self.retrieve_job = to_raw_response_wrapper(resource.retrieve_job) + + +class AsyncAmlMonitoringWithRawResponse: + def __init__(self, resource: AsyncAmlMonitoring) -> None: + self._resource = resource + self.retrieve_job = async_to_raw_response_wrapper(resource.retrieve_job) + + +class AmlMonitoringWithStreamingResponse: + def __init__(self, resource: AmlMonitoring) -> None: + self._resource = resource + self.retrieve_job = to_streamed_response_wrapper(resource.retrieve_job) + + +class AsyncAmlMonitoringWithStreamingResponse: + def __init__(self, resource: AsyncAmlMonitoring) -> None: + self._resource = resource + self.retrieve_job = async_to_streamed_response_wrapper(resource.retrieve_job) diff --git a/src/didit/resources/billing.py b/src/didit/resources/billing.py new file mode 100644 index 0000000..d11223a --- /dev/null +++ b/src/didit/resources/billing.py @@ -0,0 +1,249 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``billing`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.billing.retrieve_balance_response import BillingRetrieveBalanceResponse +from didit.types.billing.top_up_response import BillingTopUpResponse + +__all__ = [ + "Billing", + "AsyncBilling", + "BillingWithRawResponse", + "AsyncBillingWithRawResponse", + "BillingWithStreamingResponse", + "AsyncBillingWithStreamingResponse", +] + + +class Billing(SyncAPIResource): + """The ``billing`` resource of the Didit API. + + Methods: ``retrieve_balance``, ``top_up``. + """ + + @cached_property + def with_raw_response(self) -> BillingWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return BillingWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BillingWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return BillingWithStreamingResponse(self) + + def retrieve_balance( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BillingRetrieveBalanceResponse: + """Get credit balance. + + Get current pre-paid credit balance in USD (organization-wide) plus auto-refill config. Amounts are decimal strings - parse with a decimal library. + + ``GET /v3/billing/balance/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BillingRetrieveBalanceResponse", + self._client.request( + "get", + "/v3/billing/balance/", + cast_to=BillingRetrieveBalanceResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def top_up( + self, + *, + amount_in_dollars: float, + success_url: str | NotGiven = NOT_GIVEN, + cancel_url: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BillingTopUpResponse: + """Top up credits. + + Add credits via a hosted Stripe Checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. + + ``POST /v3/billing/top-up/`` + + Args: + amount_in_dollars: Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address. + success_url: URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID. + cancel_url: URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BillingTopUpResponse", + self._client.request( + "post", + "/v3/billing/top-up/", + cast_to=BillingTopUpResponse, + auth="api_key", + body={ + "amount_in_dollars": amount_in_dollars, + "success_url": success_url, + "cancel_url": cancel_url, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncBilling(AsyncAPIResource): + """The ``billing`` resource of the Didit API. + + Methods: ``retrieve_balance``, ``top_up``. + """ + + @cached_property + def with_raw_response(self) -> AsyncBillingWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncBillingWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBillingWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncBillingWithStreamingResponse(self) + + async def retrieve_balance( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BillingRetrieveBalanceResponse: + """Get credit balance. + + Get current pre-paid credit balance in USD (organization-wide) plus auto-refill config. Amounts are decimal strings - parse with a decimal library. + + ``GET /v3/billing/balance/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BillingRetrieveBalanceResponse", + await self._client.request( + "get", + "/v3/billing/balance/", + cast_to=BillingRetrieveBalanceResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def top_up( + self, + *, + amount_in_dollars: float, + success_url: str | NotGiven = NOT_GIVEN, + cancel_url: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BillingTopUpResponse: + """Top up credits. + + Add credits via a hosted Stripe Checkout session (**$50 minimum**); not available on annual plans. Redirect the customer to the returned `checkout_session_url`. + + ``POST /v3/billing/top-up/`` + + Args: + amount_in_dollars: Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address. + success_url: URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID. + cancel_url: URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BillingTopUpResponse", + await self._client.request( + "post", + "/v3/billing/top-up/", + cast_to=BillingTopUpResponse, + auth="api_key", + body={ + "amount_in_dollars": amount_in_dollars, + "success_url": success_url, + "cancel_url": cancel_url, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class BillingWithRawResponse: + def __init__(self, resource: Billing) -> None: + self._resource = resource + self.retrieve_balance = to_raw_response_wrapper(resource.retrieve_balance) + self.top_up = to_raw_response_wrapper(resource.top_up) + + +class AsyncBillingWithRawResponse: + def __init__(self, resource: AsyncBilling) -> None: + self._resource = resource + self.retrieve_balance = async_to_raw_response_wrapper(resource.retrieve_balance) + self.top_up = async_to_raw_response_wrapper(resource.top_up) + + +class BillingWithStreamingResponse: + def __init__(self, resource: Billing) -> None: + self._resource = resource + self.retrieve_balance = to_streamed_response_wrapper(resource.retrieve_balance) + self.top_up = to_streamed_response_wrapper(resource.top_up) + + +class AsyncBillingWithStreamingResponse: + def __init__(self, resource: AsyncBilling) -> None: + self._resource = resource + self.retrieve_balance = async_to_streamed_response_wrapper(resource.retrieve_balance) + self.top_up = async_to_streamed_response_wrapper(resource.top_up) diff --git a/src/didit/resources/biometric_templates.py b/src/didit/resources/biometric_templates.py new file mode 100644 index 0000000..32d6a85 --- /dev/null +++ b/src/didit/resources/biometric_templates.py @@ -0,0 +1,689 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``biometric_templates`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.biometric_templates.batch_delete_response import ( + BiometricTemplatesBatchDeleteResponse, +) +from didit.types.biometric_templates.count_response import BiometricTemplatesCountResponse +from didit.types.biometric_templates.delete_response import BiometricTemplatesDeleteResponse +from didit.types.biometric_templates.list_response import ( + BiometricTemplatesListResponse, + BiometricTemplatesListResponseResults, +) +from didit.types.biometric_templates.retrieve_response import BiometricTemplatesRetrieveResponse + +__all__ = [ + "BiometricTemplates", + "AsyncBiometricTemplates", + "BiometricTemplatesWithRawResponse", + "AsyncBiometricTemplatesWithRawResponse", + "BiometricTemplatesWithStreamingResponse", + "AsyncBiometricTemplatesWithStreamingResponse", +] + + +class BiometricTemplates(SyncAPIResource): + """The ``biometric_templates`` resource of the Didit API. + + Methods: ``batch_delete``, ``count``, ``delete``, ``list``, ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> BiometricTemplatesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return BiometricTemplatesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BiometricTemplatesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return BiometricTemplatesWithStreamingResponse(self) + + def list( + self, + *, + vendor_data: str | NotGiven = NOT_GIVEN, + vendor_user_uuid: str | NotGiven = NOT_GIVEN, + status: Literal[ + "pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed" + ] + | str + | NotGiven = NOT_GIVEN, + source_type: Literal["session_delete", "automatic_retention", "backfill"] + | str + | NotGiven = NOT_GIVEN, + retained_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + retained_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + ordering: Literal["retained_at", "-retained_at", "expires_at", "-expires_at"] + | str + | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[BiometricTemplatesListResponseResults]: + """List retained biometric templates. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Returns a paginated list of the templates the application currently holds. Purged templates are not listed. + + ``GET /v3/biometric-templates/`` + + Args: + vendor_data: Case-insensitive substring match on the owning User's current `vendor_data`. + vendor_user_uuid: Exact match on the owning User. + status: Lifecycle status. Purged templates are never listed. + source_type: How the template was created. + retained_from: Templates retained at or after this time. + retained_to: Templates retained at or before this time. Must not be earlier than `retained_from` (`400`). + ordering: Sort order. + limit: Page size. + offset: Pagination offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[BiometricTemplatesListResponseResults]", + self._client.request_page( + "get", + "/v3/biometric-templates/", + cast_to=BiometricTemplatesListResponse, + item_type=BiometricTemplatesListResponseResults, + auth="api_key", + query={ + "vendor_data": vendor_data, + "vendor_user_uuid": vendor_user_uuid, + "status": status, + "source_type": source_type, + "retained_from": retained_from, + "retained_to": retained_to, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def count( + self, + *, + vendor_data: str | NotGiven = NOT_GIVEN, + vendor_user_uuid: str | NotGiven = NOT_GIVEN, + status: Literal[ + "pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed" + ] + | str + | NotGiven = NOT_GIVEN, + source_type: Literal["session_delete", "automatic_retention", "backfill"] + | str + | NotGiven = NOT_GIVEN, + retained_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + retained_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesCountResponse: + """Count retained biometric templates. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Returns the number of templates matching the same filters as the list endpoint. Use it to check what a filtered bulk purge with `delete_all: true` would touch. + + ``GET /v3/biometric-templates/count/`` + + Args: + vendor_data: Case-insensitive substring match on the owning User's current `vendor_data`. + vendor_user_uuid: Exact match on the owning User. + status: Lifecycle status. Purged templates are never listed. + source_type: How the template was created. + retained_from: Templates retained at or after this time. + retained_to: Templates retained at or before this time. Must not be earlier than `retained_from` (`400`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesCountResponse", + self._client.request( + "get", + "/v3/biometric-templates/count/", + cast_to=BiometricTemplatesCountResponse, + auth="api_key", + query={ + "vendor_data": vendor_data, + "vendor_user_uuid": vendor_user_uuid, + "status": status, + "source_type": source_type, + "retained_from": retained_from, + "retained_to": retained_to, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesRetrieveResponse: + """Get a retained biometric template. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Returns one template. The response never includes an image, a vector, the deleted session's id, or identity data. + + ``GET /v3/biometric-templates/{template_uuid}/`` + + Args: + template_uuid: Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesRetrieveResponse", + self._client.request( + "get", + "/v3/biometric-templates/{template_uuid}/", + cast_to=BiometricTemplatesRetrieveResponse, + path_params={"template_uuid": template_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesDeleteResponse: + """Purge a retained biometric template. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Purges one template: it is removed from every kind of matching and the stored template is deleted. Irreversible and recorded on the audit trail with the acting principal. + + **Retry semantics:** Didit deletes the stored template first and only then marks the template purged. If a biometric store is unavailable the call returns `503`, the template moves to `status: purge_failed` and is already excluded from matching; repeat the same call to finish. Repeating a purge of an already purged template returns `already_purged`. + + Templates are also purged by `POST /v3/users/delete/` for the owning User, by any deletion of that User's sessions with `deletion_instruction: privacy_erasure`, and automatically at `expires_at`. + + ``DELETE /v3/biometric-templates/{template_uuid}/`` + + Args: + template_uuid: Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesDeleteResponse", + self._client.request( + "delete", + "/v3/biometric-templates/{template_uuid}/", + cast_to=BiometricTemplatesDeleteResponse, + path_params={"template_uuid": template_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def batch_delete( + self, + *, + template_uuids: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + vendor_user_uuid: str | NotGiven = NOT_GIVEN, + status: Literal[ + "pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed" + ] + | str + | NotGiven = NOT_GIVEN, + source_type: Literal["session_delete", "automatic_retention", "backfill"] + | str + | NotGiven = NOT_GIVEN, + retained_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + retained_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesBatchDeleteResponse: + """Bulk purge retained biometric templates. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Purges many templates in one call, by explicit id list or with `delete_all: true` optionally narrowed by the same filters as the list endpoint. Templates are purged one by one and each result reports `purged` or `already_purged`. If a biometric store is unavailable the call returns `503` at the first failing template: templates purged earlier in the request stay purged, the failing one moves to `purge_failed` (already excluded from matching), and repeating the request finishes the job. + + **`delete_all: true` without filters purges every biometric template in the application and cannot be undone.** Run the count endpoint with the same filters first. + + ``POST /v3/biometric-templates/delete/`` + + Args: + template_uuids: Templates to purge. Required unless `delete_all` is `true`. Already purged ids are reported as `already_purged`. + delete_all: When `true`, purges every template matching the filters below (all of the application's templates when no filter is given). `template_uuids` is ignored. Irreversible. + vendor_data: Only with `delete_all`: case-insensitive substring match on the owning User's `vendor_data`. + vendor_user_uuid: Only with `delete_all`: exact match on the owning User. + status: Only with `delete_all`. + source_type: Only with `delete_all`. + retained_from: Only with `delete_all`. + retained_to: Only with `delete_all`. Must not be earlier than `retained_from`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesBatchDeleteResponse", + self._client.request( + "post", + "/v3/biometric-templates/delete/", + cast_to=BiometricTemplatesBatchDeleteResponse, + auth="api_key", + body={ + "template_uuids": template_uuids, + "delete_all": delete_all, + "vendor_data": vendor_data, + "vendor_user_uuid": vendor_user_uuid, + "status": status, + "source_type": source_type, + "retained_from": retained_from, + "retained_to": retained_to, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncBiometricTemplates(AsyncAPIResource): + """The ``biometric_templates`` resource of the Didit API. + + Methods: ``batch_delete``, ``count``, ``delete``, ``list``, ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> AsyncBiometricTemplatesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncBiometricTemplatesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBiometricTemplatesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncBiometricTemplatesWithStreamingResponse(self) + + def list( + self, + *, + vendor_data: str | NotGiven = NOT_GIVEN, + vendor_user_uuid: str | NotGiven = NOT_GIVEN, + status: Literal[ + "pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed" + ] + | str + | NotGiven = NOT_GIVEN, + source_type: Literal["session_delete", "automatic_retention", "backfill"] + | str + | NotGiven = NOT_GIVEN, + retained_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + retained_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + ordering: Literal["retained_at", "-retained_at", "expires_at", "-expires_at"] + | str + | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[BiometricTemplatesListResponseResults]: + """List retained biometric templates. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Returns a paginated list of the templates the application currently holds. Purged templates are not listed. + + ``GET /v3/biometric-templates/`` + + Args: + vendor_data: Case-insensitive substring match on the owning User's current `vendor_data`. + vendor_user_uuid: Exact match on the owning User. + status: Lifecycle status. Purged templates are never listed. + source_type: How the template was created. + retained_from: Templates retained at or after this time. + retained_to: Templates retained at or before this time. Must not be earlier than `retained_from` (`400`). + ordering: Sort order. + limit: Page size. + offset: Pagination offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[BiometricTemplatesListResponseResults]", + self._client.request_page( + "get", + "/v3/biometric-templates/", + cast_to=BiometricTemplatesListResponse, + item_type=BiometricTemplatesListResponseResults, + auth="api_key", + query={ + "vendor_data": vendor_data, + "vendor_user_uuid": vendor_user_uuid, + "status": status, + "source_type": source_type, + "retained_from": retained_from, + "retained_to": retained_to, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def count( + self, + *, + vendor_data: str | NotGiven = NOT_GIVEN, + vendor_user_uuid: str | NotGiven = NOT_GIVEN, + status: Literal[ + "pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed" + ] + | str + | NotGiven = NOT_GIVEN, + source_type: Literal["session_delete", "automatic_retention", "backfill"] + | str + | NotGiven = NOT_GIVEN, + retained_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + retained_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesCountResponse: + """Count retained biometric templates. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Returns the number of templates matching the same filters as the list endpoint. Use it to check what a filtered bulk purge with `delete_all: true` would touch. + + ``GET /v3/biometric-templates/count/`` + + Args: + vendor_data: Case-insensitive substring match on the owning User's current `vendor_data`. + vendor_user_uuid: Exact match on the owning User. + status: Lifecycle status. Purged templates are never listed. + source_type: How the template was created. + retained_from: Templates retained at or after this time. + retained_to: Templates retained at or before this time. Must not be earlier than `retained_from` (`400`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesCountResponse", + await self._client.request( + "get", + "/v3/biometric-templates/count/", + cast_to=BiometricTemplatesCountResponse, + auth="api_key", + query={ + "vendor_data": vendor_data, + "vendor_user_uuid": vendor_user_uuid, + "status": status, + "source_type": source_type, + "retained_from": retained_from, + "retained_to": retained_to, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesRetrieveResponse: + """Get a retained biometric template. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Returns one template. The response never includes an image, a vector, the deleted session's id, or identity data. + + ``GET /v3/biometric-templates/{template_uuid}/`` + + Args: + template_uuid: Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesRetrieveResponse", + await self._client.request( + "get", + "/v3/biometric-templates/{template_uuid}/", + cast_to=BiometricTemplatesRetrieveResponse, + path_params={"template_uuid": template_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesDeleteResponse: + """Purge a retained biometric template. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Purges one template: it is removed from every kind of matching and the stored template is deleted. Irreversible and recorded on the audit trail with the acting principal. + + **Retry semantics:** Didit deletes the stored template first and only then marks the template purged. If a biometric store is unavailable the call returns `503`, the template moves to `status: purge_failed` and is already excluded from matching; repeat the same call to finish. Repeating a purge of an already purged template returns `already_purged`. + + Templates are also purged by `POST /v3/users/delete/` for the owning User, by any deletion of that User's sessions with `deletion_instruction: privacy_erasure`, and automatically at `expires_at`. + + ``DELETE /v3/biometric-templates/{template_uuid}/`` + + Args: + template_uuid: Template id, as returned in `biometric_template_uuid` by the session deletion endpoints, `biometric_template_id` on a Face Search match, or the list endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesDeleteResponse", + await self._client.request( + "delete", + "/v3/biometric-templates/{template_uuid}/", + cast_to=BiometricTemplatesDeleteResponse, + path_params={"template_uuid": template_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def batch_delete( + self, + *, + template_uuids: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + vendor_user_uuid: str | NotGiven = NOT_GIVEN, + status: Literal[ + "pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed" + ] + | str + | NotGiven = NOT_GIVEN, + source_type: Literal["session_delete", "automatic_retention", "backfill"] + | str + | NotGiven = NOT_GIVEN, + retained_from: str | _dt.datetime | NotGiven = NOT_GIVEN, + retained_to: str | _dt.datetime | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BiometricTemplatesBatchDeleteResponse: + """Bulk purge retained biometric templates. + + An opted-in application keeps one separately managed, image-free face biometric template anchored to the User when a session is deleted (see `DELETE /v3/session/{sessionId}/delete/`). Templates are biometric data with a finite `expires_at`; they never contain images, session data, or identity fields, and they are purged on User deletion, privacy erasure, expiry, or an explicit purge. These endpoints accept application API keys (`x-api-key`) only; Business Console user tokens are rejected with `403`. + + Purges many templates in one call, by explicit id list or with `delete_all: true` optionally narrowed by the same filters as the list endpoint. Templates are purged one by one and each result reports `purged` or `already_purged`. If a biometric store is unavailable the call returns `503` at the first failing template: templates purged earlier in the request stay purged, the failing one moves to `purge_failed` (already excluded from matching), and repeating the request finishes the job. + + **`delete_all: true` without filters purges every biometric template in the application and cannot be undone.** Run the count endpoint with the same filters first. + + ``POST /v3/biometric-templates/delete/`` + + Args: + template_uuids: Templates to purge. Required unless `delete_all` is `true`. Already purged ids are reported as `already_purged`. + delete_all: When `true`, purges every template matching the filters below (all of the application's templates when no filter is given). `template_uuids` is ignored. Irreversible. + vendor_data: Only with `delete_all`: case-insensitive substring match on the owning User's `vendor_data`. + vendor_user_uuid: Only with `delete_all`: exact match on the owning User. + status: Only with `delete_all`. + source_type: Only with `delete_all`. + retained_from: Only with `delete_all`. + retained_to: Only with `delete_all`. Must not be earlier than `retained_from`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BiometricTemplatesBatchDeleteResponse", + await self._client.request( + "post", + "/v3/biometric-templates/delete/", + cast_to=BiometricTemplatesBatchDeleteResponse, + auth="api_key", + body={ + "template_uuids": template_uuids, + "delete_all": delete_all, + "vendor_data": vendor_data, + "vendor_user_uuid": vendor_user_uuid, + "status": status, + "source_type": source_type, + "retained_from": retained_from, + "retained_to": retained_to, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class BiometricTemplatesWithRawResponse: + def __init__(self, resource: BiometricTemplates) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.count = to_raw_response_wrapper(resource.count) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.batch_delete = to_raw_response_wrapper(resource.batch_delete) + + +class AsyncBiometricTemplatesWithRawResponse: + def __init__(self, resource: AsyncBiometricTemplates) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.count = async_to_raw_response_wrapper(resource.count) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.batch_delete = async_to_raw_response_wrapper(resource.batch_delete) + + +class BiometricTemplatesWithStreamingResponse: + def __init__(self, resource: BiometricTemplates) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.count = to_streamed_response_wrapper(resource.count) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.batch_delete = to_streamed_response_wrapper(resource.batch_delete) + + +class AsyncBiometricTemplatesWithStreamingResponse: + def __init__(self, resource: AsyncBiometricTemplates) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.count = async_to_streamed_response_wrapper(resource.count) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.batch_delete = async_to_streamed_response_wrapper(resource.batch_delete) diff --git a/src/didit/resources/business_sessions.py b/src/didit/resources/business_sessions.py new file mode 100644 index 0000000..af5d6bb --- /dev/null +++ b/src/didit/resources/business_sessions.py @@ -0,0 +1,164 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``business_sessions`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.business_sessions.list_networks_response import ( + BusinessSessionsListNetworksResponse, +) + +__all__ = [ + "BusinessSessions", + "AsyncBusinessSessions", + "BusinessSessionsWithRawResponse", + "AsyncBusinessSessionsWithRawResponse", + "BusinessSessionsWithStreamingResponse", + "AsyncBusinessSessionsWithStreamingResponse", +] + + +class BusinessSessions(SyncAPIResource): + """The ``business_sessions`` resource of the Didit API. + + Methods: ``list_networks``. + """ + + @cached_property + def with_raw_response(self) -> BusinessSessionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return BusinessSessionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BusinessSessionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return BusinessSessionsWithStreamingResponse(self) + + def list_networks( + self, + business_session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessSessionsListNetworksResponse: + """List networks for a business session. + + List the networks the business session's vendor business belongs to. + + ``GET /v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/`` + + Args: + business_session_id: Business session UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessSessionsListNetworksResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/", + cast_to=BusinessSessionsListNetworksResponse, + path_params={"business_session_id": business_session_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncBusinessSessions(AsyncAPIResource): + """The ``business_sessions`` resource of the Didit API. + + Methods: ``list_networks``. + """ + + @cached_property + def with_raw_response(self) -> AsyncBusinessSessionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncBusinessSessionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBusinessSessionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncBusinessSessionsWithStreamingResponse(self) + + async def list_networks( + self, + business_session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessSessionsListNetworksResponse: + """List networks for a business session. + + List the networks the business session's vendor business belongs to. + + ``GET /v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/`` + + Args: + business_session_id: Business session UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessSessionsListNetworksResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/", + cast_to=BusinessSessionsListNetworksResponse, + path_params={"business_session_id": business_session_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class BusinessSessionsWithRawResponse: + def __init__(self, resource: BusinessSessions) -> None: + self._resource = resource + self.list_networks = to_raw_response_wrapper(resource.list_networks) + + +class AsyncBusinessSessionsWithRawResponse: + def __init__(self, resource: AsyncBusinessSessions) -> None: + self._resource = resource + self.list_networks = async_to_raw_response_wrapper(resource.list_networks) + + +class BusinessSessionsWithStreamingResponse: + def __init__(self, resource: BusinessSessions) -> None: + self._resource = resource + self.list_networks = to_streamed_response_wrapper(resource.list_networks) + + +class AsyncBusinessSessionsWithStreamingResponse: + def __init__(self, resource: AsyncBusinessSessions) -> None: + self._resource = resource + self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) diff --git a/src/didit/resources/businesses.py b/src/didit/resources/businesses.py new file mode 100644 index 0000000..76350e9 --- /dev/null +++ b/src/didit/resources/businesses.py @@ -0,0 +1,1379 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``businesses`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.businesses.batch_delete_response import BusinessesBatchDeleteResponse +from didit.types.businesses.list_networks_response import BusinessesListNetworksResponse +from didit.types.businesses.list_response import BusinessesListResponse +from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( + BusinessesRetrieveOngoingMonitoringJobResponse, +) +from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( + BusinessesRetrieveSessionOngoingMonitoringJobResponse, +) +from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponse202, + BusinessesSetAmlMonitoringResponseVariant1, + BusinessesSetAmlMonitoringResponseVariant2, +) +from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponse202, + BusinessesSetOngoingMonitoringResponseVariant1, + BusinessesSetOngoingMonitoringResponseVariant2, +) +from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponse202, + BusinessesSetSessionOngoingMonitoringResponseVariant1, + BusinessesSetSessionOngoingMonitoringResponseVariant2, +) +from didit.types.components.business_detail_item import BusinessDetailItem +from didit.types.components.business_list_item import BusinessListItem + +__all__ = [ + "Businesses", + "AsyncBusinesses", + "BusinessesWithRawResponse", + "AsyncBusinessesWithRawResponse", + "BusinessesWithStreamingResponse", + "AsyncBusinessesWithStreamingResponse", +] + + +class Businesses(SyncAPIResource): + """The ``businesses`` resource of the Didit API. + + Methods: ``batch_delete``, ``create``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``retrieve_session_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``set_session_ongoing_monitoring``, ``update``, ``update_status``. + """ + + @cached_property + def with_raw_response(self) -> BusinessesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return BusinessesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> BusinessesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return BusinessesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[BusinessListItem]: + """List businesses. + + List [businesses](/entities/businesses) Didit assembles from KYB sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus businesses created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters - fetch pages and filter client-side. + + ``GET /v3/businesses/`` + + Args: + limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. + offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[BusinessListItem]", + self._client.request_page( + "get", + "/v3/businesses/", + cast_to=BusinessesListResponse, + item_type=BusinessListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + vendor_data: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Get business. + + Fetch one [business](/entities/businesses) by `vendor_data` (exact match). Adds `metadata` and `updated_at` to the list view. Deleted businesses return 404. + + ``GET /v3/businesses/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the business - a free-form string (NOT a UUID), matched exactly as sent. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + self._client.request( + "get", + "/v3/businesses/{vendor_data}/", + cast_to=BusinessDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + vendor_data: str, + *, + display_name: str | None | NotGiven = NOT_GIVEN, + legal_name: str | None | NotGiven = NOT_GIVEN, + registration_number: str | None | NotGiven = NOT_GIVEN, + country_code: str | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Update business. + + Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. + + ``PATCH /v3/businesses/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the business (free-form string, NOT a UUID). + display_name: Friendly display name shown in the console (takes precedence over `legal_name` for UI display). + legal_name: Official legal name from registry or manual entry. + registration_number: Company registration or incorporation number. + country_code: Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`). + status: Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry. + metadata: Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + self._client.request( + "patch", + "/v3/businesses/{vendor_data}/", + cast_to=BusinessDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={ + "display_name": display_name, + "legal_name": legal_name, + "registration_number": registration_number, + "country_code": country_code, + "status": status, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_status( + self, + vendor_data: str, + *, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Update business status. + + Flip only the lifecycle `status` of a business - VendorBusinessStatusChoices (`ACTIVE`/`FLAGGED`/`BLOCKED`, NOT session statuses). `BLOCKED` adds `vendor_data` to the system blocklist. + + ``PATCH /v3/businesses/{vendor_data}/update-status/`` + + Args: + vendor_data: Your unique identifier for the business (free-form string, NOT a UUID). + status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + self._client.request( + "patch", + "/v3/businesses/{vendor_data}/update-status/", + cast_to=BusinessDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={"status": status}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def batch_delete( + self, + *, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesBatchDeleteResponse: + """Batch delete businesses. + + **Permanently delete** businesses - this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`. + + ``POST /v3/businesses/delete/`` + + Args: + vendor_data_list: Your own identifiers to delete, matched exactly as sent. + didit_internal_id_list: Didit's internal UUIDs (`didit_internal_id`) to delete. + delete_all: If `true`, deletes every business in the application. Cannot be combined with the list selectors. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesBatchDeleteResponse", + self._client.request( + "post", + "/v3/businesses/delete/", + cast_to=BusinessesBatchDeleteResponse, + auth="api_key", + body={ + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + "delete_all": delete_all, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def set_aml_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + BusinessesSetAmlMonitoringResponseVariant1 + | BusinessesSetAmlMonitoringResponseVariant2 + | BusinessesSetAmlMonitoringResponse202 + ): + """Enable or disable AML ongoing monitoring for businesses. + + Turns AML ongoing monitoring on or off for the selected businesses, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per business, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Businesses without any AML screening are screened first (billed as a regular AML screening) when they have the legal or display name needed; otherwise they are reported as `skipped_no_identity_data`. + + Set `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints. + + ``POST /v3/businesses/aml-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes - no screening, no billing, no job. + vendor_data_list: Your own identifiers of the businesses to toggle, matched exactly as sent. + didit_internal_id_list: Didit internal ids of the businesses to toggle - the `didit_internal_id` returned by the create, get and list endpoints. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesSetAmlMonitoringResponseVariant1 | BusinessesSetAmlMonitoringResponseVariant2 | BusinessesSetAmlMonitoringResponse202", + self._client.request( + "post", + "/v3/businesses/aml-monitoring/", + cast_to={ + 200: BusinessesSetAmlMonitoringResponseVariant1 + | BusinessesSetAmlMonitoringResponseVariant2, + 202: BusinessesSetAmlMonitoringResponse202, + }, + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + vendor_data: str | None | NotGiven = NOT_GIVEN, + display_name: str | None | NotGiven = NOT_GIVEN, + legal_name: str | None | NotGiven = NOT_GIVEN, + registration_number: str | None | NotGiven = NOT_GIVEN, + country_code: str | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Create business. + + Pre-create a [business](/entities/businesses) *without* a KYB session. All fields are optional, but you should always send `vendor_data` - without it the record cannot be linked to sessions or transactions. When provided, `vendor_data` must be unique among non-deleted businesses (exact match; conflicts return 400). Not idempotent. + + ``POST /v3/businesses/create/`` + + Args: + vendor_data: Your unique identifier for this business (free-form string, NOT a UUID). Optional but strongly recommended. When provided, must be unique among non-deleted businesses for the application; matched exactly as sent. + display_name: Friendly display name shown in the console (takes precedence over `legal_name` for UI display). + legal_name: Official legal name of the company. + registration_number: Company registration or incorporation number. + country_code: Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`). + status: Initial lifecycle status. Defaults to `ACTIVE`. + metadata: Arbitrary JSON object you attach to the business. Defaults to `{}`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + self._client.request( + "post", + "/v3/businesses/create/", + cast_to=BusinessDetailItem, + auth="api_key", + body={ + "vendor_data": vendor_data, + "display_name": display_name, + "legal_name": legal_name, + "registration_number": registration_number, + "country_code": country_code, + "status": status, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def set_ongoing_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + update_all: bool | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_status: Sequence[str] | NotGiven = NOT_GIVEN, + filter_country: str | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + BusinessesSetOngoingMonitoringResponseVariant1 + | BusinessesSetOngoingMonitoringResponseVariant2 + | BusinessesSetOngoingMonitoringResponse202 + ): + """Toggle AML ongoing monitoring for businesses. + + Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes. + update_all: Apply to every entity of the application (scoped by the filter_* params when present). + filter_search + filter_status + filter_country + filter_date_from + filter_date_to + didit_internal_id_list: Entities to toggle. Required unless update_all is true. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesSetOngoingMonitoringResponseVariant1 | BusinessesSetOngoingMonitoringResponseVariant2 | BusinessesSetOngoingMonitoringResponse202", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/", + cast_to={ + 200: BusinessesSetOngoingMonitoringResponseVariant1 + | BusinessesSetOngoingMonitoringResponseVariant2, + 202: BusinessesSetOngoingMonitoringResponse202, + }, + scope="application", + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "update_all": update_all, + "filter_search": filter_search, + "filter_status": filter_status, + "filter_country": filter_country, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_ongoing_monitoring_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesRetrieveOngoingMonitoringJobResponse: + """Get AML ongoing monitoring job. + + Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. + + ``GET /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/`` + + Args: + job_id: Monitoring job UUID returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesRetrieveOngoingMonitoringJobResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/", + cast_to=BusinessesRetrieveOngoingMonitoringJobResponse, + path_params={"job_id": job_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def set_session_ongoing_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + update_all: bool | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_status: Sequence[str] | NotGiven = NOT_GIVEN, + filter_country: str | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + session_numbers: Sequence[str] | NotGiven = NOT_GIVEN, + toggle_all: bool | NotGiven = NOT_GIVEN, + filter_workflow_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + BusinessesSetSessionOngoingMonitoringResponseVariant1 + | BusinessesSetSessionOngoingMonitoringResponseVariant2 + | BusinessesSetSessionOngoingMonitoringResponse202 + ): + """Toggle AML ongoing monitoring for business sessions. + + Session-level toggle for KYB verifications, addressed by session number. Sessions without an AML screening get one created and attached to the session itself. Same estimate_only, inline fast-path and async job contract as the user-level endpoints. Replaces the legacy synchronous behavior that skipped sessions without an AML screening. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes. + update_all: Apply to every entity of the application (scoped by the filter_* params when present). + filter_search + filter_status + filter_country + filter_date_from + filter_date_to + session_numbers: Business session numbers to toggle. Required unless update_all (or the legacy toggle_all alias) is true. + toggle_all: Legacy alias of update_all. + filter_workflow_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesSetSessionOngoingMonitoringResponseVariant1 | BusinessesSetSessionOngoingMonitoringResponseVariant2 | BusinessesSetSessionOngoingMonitoringResponse202", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/", + cast_to={ + 200: BusinessesSetSessionOngoingMonitoringResponseVariant1 + | BusinessesSetSessionOngoingMonitoringResponseVariant2, + 202: BusinessesSetSessionOngoingMonitoringResponse202, + }, + scope="application", + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "update_all": update_all, + "filter_search": filter_search, + "filter_status": filter_status, + "filter_country": filter_country, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "session_numbers": session_numbers, + "toggle_all": toggle_all, + "filter_workflow_id": filter_workflow_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_session_ongoing_monitoring_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesRetrieveSessionOngoingMonitoringJobResponse: + """Get AML ongoing monitoring job. + + Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. + + ``GET /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/`` + + Args: + job_id: Monitoring job UUID returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesRetrieveSessionOngoingMonitoringJobResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/", + cast_to=BusinessesRetrieveSessionOngoingMonitoringJobResponse, + path_params={"job_id": job_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_networks( + self, + vendor_business_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesListNetworksResponse: + """List networks for a business. + + List the networks a vendor business belongs to. + + ``GET /v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/`` + + Args: + vendor_business_uuid: Vendor business UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesListNetworksResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/", + cast_to=BusinessesListNetworksResponse, + path_params={"vendor_business_uuid": vendor_business_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncBusinesses(AsyncAPIResource): + """The ``businesses`` resource of the Didit API. + + Methods: ``batch_delete``, ``create``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``retrieve_session_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``set_session_ongoing_monitoring``, ``update``, ``update_status``. + """ + + @cached_property + def with_raw_response(self) -> AsyncBusinessesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncBusinessesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncBusinessesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncBusinessesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[BusinessListItem]: + """List businesses. + + List [businesses](/entities/businesses) Didit assembles from KYB sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus businesses created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters - fetch pages and filter client-side. + + ``GET /v3/businesses/`` + + Args: + limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. + offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[BusinessListItem]", + self._client.request_page( + "get", + "/v3/businesses/", + cast_to=BusinessesListResponse, + item_type=BusinessListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + vendor_data: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Get business. + + Fetch one [business](/entities/businesses) by `vendor_data` (exact match). Adds `metadata` and `updated_at` to the list view. Deleted businesses return 404. + + ``GET /v3/businesses/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the business - a free-form string (NOT a UUID), matched exactly as sent. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + await self._client.request( + "get", + "/v3/businesses/{vendor_data}/", + cast_to=BusinessDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + vendor_data: str, + *, + display_name: str | None | NotGiven = NOT_GIVEN, + legal_name: str | None | NotGiven = NOT_GIVEN, + registration_number: str | None | NotGiven = NOT_GIVEN, + country_code: str | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Update business. + + Partial update of a business: `display_name`, `legal_name`, `registration_number`, `country_code`, `status`, `metadata`. `metadata` fully replaces. Setting `status` to `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. + + ``PATCH /v3/businesses/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the business (free-form string, NOT a UUID). + display_name: Friendly display name shown in the console (takes precedence over `legal_name` for UI display). + legal_name: Official legal name from registry or manual entry. + registration_number: Company registration or incorporation number. + country_code: Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`). + status: Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry. + metadata: Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + await self._client.request( + "patch", + "/v3/businesses/{vendor_data}/", + cast_to=BusinessDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={ + "display_name": display_name, + "legal_name": legal_name, + "registration_number": registration_number, + "country_code": country_code, + "status": status, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_status( + self, + vendor_data: str, + *, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Update business status. + + Flip only the lifecycle `status` of a business - VendorBusinessStatusChoices (`ACTIVE`/`FLAGGED`/`BLOCKED`, NOT session statuses). `BLOCKED` adds `vendor_data` to the system blocklist. + + ``PATCH /v3/businesses/{vendor_data}/update-status/`` + + Args: + vendor_data: Your unique identifier for the business (free-form string, NOT a UUID). + status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + await self._client.request( + "patch", + "/v3/businesses/{vendor_data}/update-status/", + cast_to=BusinessDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={"status": status}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def batch_delete( + self, + *, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesBatchDeleteResponse: + """Batch delete businesses. + + **Permanently delete** businesses - this cannot be undone. For blocking prefer [Update Business Status](#patch-/v3/businesses/-vendor_data-/update-status/) with `BLOCKED`. + + ``POST /v3/businesses/delete/`` + + Args: + vendor_data_list: Your own identifiers to delete, matched exactly as sent. + didit_internal_id_list: Didit's internal UUIDs (`didit_internal_id`) to delete. + delete_all: If `true`, deletes every business in the application. Cannot be combined with the list selectors. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesBatchDeleteResponse", + await self._client.request( + "post", + "/v3/businesses/delete/", + cast_to=BusinessesBatchDeleteResponse, + auth="api_key", + body={ + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + "delete_all": delete_all, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def set_aml_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + BusinessesSetAmlMonitoringResponseVariant1 + | BusinessesSetAmlMonitoringResponseVariant2 + | BusinessesSetAmlMonitoringResponse202 + ): + """Enable or disable AML ongoing monitoring for businesses. + + Turns AML ongoing monitoring on or off for the selected businesses, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per business, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Businesses without any AML screening are screened first (billed as a regular AML screening) when they have the legal or display name needed; otherwise they are reported as `skipped_no_identity_data`. + + Set `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints. + + ``POST /v3/businesses/aml-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes - no screening, no billing, no job. + vendor_data_list: Your own identifiers of the businesses to toggle, matched exactly as sent. + didit_internal_id_list: Didit internal ids of the businesses to toggle - the `didit_internal_id` returned by the create, get and list endpoints. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesSetAmlMonitoringResponseVariant1 | BusinessesSetAmlMonitoringResponseVariant2 | BusinessesSetAmlMonitoringResponse202", + await self._client.request( + "post", + "/v3/businesses/aml-monitoring/", + cast_to={ + 200: BusinessesSetAmlMonitoringResponseVariant1 + | BusinessesSetAmlMonitoringResponseVariant2, + 202: BusinessesSetAmlMonitoringResponse202, + }, + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + vendor_data: str | None | NotGiven = NOT_GIVEN, + display_name: str | None | NotGiven = NOT_GIVEN, + legal_name: str | None | NotGiven = NOT_GIVEN, + registration_number: str | None | NotGiven = NOT_GIVEN, + country_code: str | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessDetailItem: + """Create business. + + Pre-create a [business](/entities/businesses) *without* a KYB session. All fields are optional, but you should always send `vendor_data` - without it the record cannot be linked to sessions or transactions. When provided, `vendor_data` must be unique among non-deleted businesses (exact match; conflicts return 400). Not idempotent. + + ``POST /v3/businesses/create/`` + + Args: + vendor_data: Your unique identifier for this business (free-form string, NOT a UUID). Optional but strongly recommended. When provided, must be unique among non-deleted businesses for the application; matched exactly as sent. + display_name: Friendly display name shown in the console (takes precedence over `legal_name` for UI display). + legal_name: Official legal name of the company. + registration_number: Company registration or incorporation number. + country_code: Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`). + status: Initial lifecycle status. Defaults to `ACTIVE`. + metadata: Arbitrary JSON object you attach to the business. Defaults to `{}`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessDetailItem", + await self._client.request( + "post", + "/v3/businesses/create/", + cast_to=BusinessDetailItem, + auth="api_key", + body={ + "vendor_data": vendor_data, + "display_name": display_name, + "legal_name": legal_name, + "registration_number": registration_number, + "country_code": country_code, + "status": status, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def set_ongoing_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + update_all: bool | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_status: Sequence[str] | NotGiven = NOT_GIVEN, + filter_country: str | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + BusinessesSetOngoingMonitoringResponseVariant1 + | BusinessesSetOngoingMonitoringResponseVariant2 + | BusinessesSetOngoingMonitoringResponse202 + ): + """Toggle AML ongoing monitoring for businesses. + + Business-level equivalent of the user toggle: one monitored AML screening per business, with businesses lacking any AML screening screened first (company entity). Same estimate_only, inline fast-path and async job contract. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes. + update_all: Apply to every entity of the application (scoped by the filter_* params when present). + filter_search + filter_status + filter_country + filter_date_from + filter_date_to + didit_internal_id_list: Entities to toggle. Required unless update_all is true. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesSetOngoingMonitoringResponseVariant1 | BusinessesSetOngoingMonitoringResponseVariant2 | BusinessesSetOngoingMonitoringResponse202", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/", + cast_to={ + 200: BusinessesSetOngoingMonitoringResponseVariant1 + | BusinessesSetOngoingMonitoringResponseVariant2, + 202: BusinessesSetOngoingMonitoringResponse202, + }, + scope="application", + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "update_all": update_all, + "filter_search": filter_search, + "filter_status": filter_status, + "filter_country": filter_country, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_ongoing_monitoring_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesRetrieveOngoingMonitoringJobResponse: + """Get AML ongoing monitoring job. + + Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. + + ``GET /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/`` + + Args: + job_id: Monitoring job UUID returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesRetrieveOngoingMonitoringJobResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/", + cast_to=BusinessesRetrieveOngoingMonitoringJobResponse, + path_params={"job_id": job_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def set_session_ongoing_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + update_all: bool | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_status: Sequence[str] | NotGiven = NOT_GIVEN, + filter_country: str | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + session_numbers: Sequence[str] | NotGiven = NOT_GIVEN, + toggle_all: bool | NotGiven = NOT_GIVEN, + filter_workflow_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + BusinessesSetSessionOngoingMonitoringResponseVariant1 + | BusinessesSetSessionOngoingMonitoringResponseVariant2 + | BusinessesSetSessionOngoingMonitoringResponse202 + ): + """Toggle AML ongoing monitoring for business sessions. + + Session-level toggle for KYB verifications, addressed by session number. Sessions without an AML screening get one created and attached to the session itself. Same estimate_only, inline fast-path and async job contract as the user-level endpoints. Replaces the legacy synchronous behavior that skipped sessions without an AML screening. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes. + update_all: Apply to every entity of the application (scoped by the filter_* params when present). + filter_search + filter_status + filter_country + filter_date_from + filter_date_to + session_numbers: Business session numbers to toggle. Required unless update_all (or the legacy toggle_all alias) is true. + toggle_all: Legacy alias of update_all. + filter_workflow_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesSetSessionOngoingMonitoringResponseVariant1 | BusinessesSetSessionOngoingMonitoringResponseVariant2 | BusinessesSetSessionOngoingMonitoringResponse202", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/", + cast_to={ + 200: BusinessesSetSessionOngoingMonitoringResponseVariant1 + | BusinessesSetSessionOngoingMonitoringResponseVariant2, + 202: BusinessesSetSessionOngoingMonitoringResponse202, + }, + scope="application", + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "update_all": update_all, + "filter_search": filter_search, + "filter_status": filter_status, + "filter_country": filter_country, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "session_numbers": session_numbers, + "toggle_all": toggle_all, + "filter_workflow_id": filter_workflow_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_session_ongoing_monitoring_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesRetrieveSessionOngoingMonitoringJobResponse: + """Get AML ongoing monitoring job. + + Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. + + ``GET /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/`` + + Args: + job_id: Monitoring job UUID returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesRetrieveSessionOngoingMonitoringJobResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/", + cast_to=BusinessesRetrieveSessionOngoingMonitoringJobResponse, + path_params={"job_id": job_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_networks( + self, + vendor_business_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BusinessesListNetworksResponse: + """List networks for a business. + + List the networks a vendor business belongs to. + + ``GET /v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/`` + + Args: + vendor_business_uuid: Vendor business UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BusinessesListNetworksResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/", + cast_to=BusinessesListNetworksResponse, + path_params={"vendor_business_uuid": vendor_business_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class BusinessesWithRawResponse: + def __init__(self, resource: Businesses) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.update = to_raw_response_wrapper(resource.update) + self.update_status = to_raw_response_wrapper(resource.update_status) + self.batch_delete = to_raw_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = to_raw_response_wrapper(resource.set_aml_monitoring) + self.create = to_raw_response_wrapper(resource.create) + self.set_ongoing_monitoring = to_raw_response_wrapper(resource.set_ongoing_monitoring) + self.retrieve_ongoing_monitoring_job = to_raw_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.set_session_ongoing_monitoring = to_raw_response_wrapper( + resource.set_session_ongoing_monitoring + ) + self.retrieve_session_ongoing_monitoring_job = to_raw_response_wrapper( + resource.retrieve_session_ongoing_monitoring_job + ) + self.list_networks = to_raw_response_wrapper(resource.list_networks) + + +class AsyncBusinessesWithRawResponse: + def __init__(self, resource: AsyncBusinesses) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.update = async_to_raw_response_wrapper(resource.update) + self.update_status = async_to_raw_response_wrapper(resource.update_status) + self.batch_delete = async_to_raw_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = async_to_raw_response_wrapper(resource.set_aml_monitoring) + self.create = async_to_raw_response_wrapper(resource.create) + self.set_ongoing_monitoring = async_to_raw_response_wrapper(resource.set_ongoing_monitoring) + self.retrieve_ongoing_monitoring_job = async_to_raw_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.set_session_ongoing_monitoring = async_to_raw_response_wrapper( + resource.set_session_ongoing_monitoring + ) + self.retrieve_session_ongoing_monitoring_job = async_to_raw_response_wrapper( + resource.retrieve_session_ongoing_monitoring_job + ) + self.list_networks = async_to_raw_response_wrapper(resource.list_networks) + + +class BusinessesWithStreamingResponse: + def __init__(self, resource: Businesses) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.update = to_streamed_response_wrapper(resource.update) + self.update_status = to_streamed_response_wrapper(resource.update_status) + self.batch_delete = to_streamed_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = to_streamed_response_wrapper(resource.set_aml_monitoring) + self.create = to_streamed_response_wrapper(resource.create) + self.set_ongoing_monitoring = to_streamed_response_wrapper(resource.set_ongoing_monitoring) + self.retrieve_ongoing_monitoring_job = to_streamed_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.set_session_ongoing_monitoring = to_streamed_response_wrapper( + resource.set_session_ongoing_monitoring + ) + self.retrieve_session_ongoing_monitoring_job = to_streamed_response_wrapper( + resource.retrieve_session_ongoing_monitoring_job + ) + self.list_networks = to_streamed_response_wrapper(resource.list_networks) + + +class AsyncBusinessesWithStreamingResponse: + def __init__(self, resource: AsyncBusinesses) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.update = async_to_streamed_response_wrapper(resource.update) + self.update_status = async_to_streamed_response_wrapper(resource.update_status) + self.batch_delete = async_to_streamed_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = async_to_streamed_response_wrapper(resource.set_aml_monitoring) + self.create = async_to_streamed_response_wrapper(resource.create) + self.set_ongoing_monitoring = async_to_streamed_response_wrapper( + resource.set_ongoing_monitoring + ) + self.retrieve_ongoing_monitoring_job = async_to_streamed_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.set_session_ongoing_monitoring = async_to_streamed_response_wrapper( + resource.set_session_ongoing_monitoring + ) + self.retrieve_session_ongoing_monitoring_job = async_to_streamed_response_wrapper( + resource.retrieve_session_ongoing_monitoring_job + ) + self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) diff --git a/src/didit/resources/cases/__init__.py b/src/didit/resources/cases/__init__.py new file mode 100644 index 0000000..ede07dc --- /dev/null +++ b/src/didit/resources/cases/__init__.py @@ -0,0 +1,2757 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases`` resource.""" + +from __future__ import annotations + +import builtins +import datetime as _dt +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.cases.bulk_assign_response import CasesBulkAssignResponse +from didit.types.cases.bulk_update_status_response import CasesBulkUpdateStatusResponse +from didit.types.cases.create_params import CasesCreateParamsSubject +from didit.types.cases.list_events_response import CasesListEventsResponse +from didit.types.cases.list_response import CasesListResponse +from didit.types.cases.list_reviewers_response import CasesListReviewersResponse +from didit.types.cases.print_response import CasesPrintResponse +from didit.types.cases.retrieve_statistics_response import CasesRetrieveStatisticsResponse +from didit.types.components.case_action_action_enum import CaseActionActionEnum +from didit.types.components.case_action_case_action_receipt import CaseActionCaseActionReceipt +from didit.types.components.case_action_case_action_receipt_page import ( + CaseActionCaseActionReceiptPage, +) +from didit.types.components.case_action_case_entity_action_result import ( + CaseActionCaseEntityActionResult, +) +from didit.types.components.case_detail import CaseDetail +from didit.types.components.case_duplicate_case_duplicate_page import CaseDuplicateCaseDuplicatePage +from didit.types.components.case_duplicate_case_fraud_source_context_request import ( + CaseDuplicateCaseFraudSourceContextRequest, +) +from didit.types.components.case_duplicate_case_link_create_request import ( + CaseDuplicateCaseLinkCreateRequest, +) +from didit.types.components.case_duplicate_case_subject_input_request import ( + CaseDuplicateCaseSubjectInputRequest, +) +from didit.types.components.case_event import CaseEvent +from didit.types.components.case_list_item import CaseListItem + +if TYPE_CHECKING: + from didit.resources.cases.blueprints import ( + AsyncCasesBlueprints, + AsyncCasesBlueprintsWithRawResponse, + AsyncCasesBlueprintsWithStreamingResponse, + CasesBlueprints, + CasesBlueprintsWithRawResponse, + CasesBlueprintsWithStreamingResponse, + ) + from didit.resources.cases.checklist import ( + AsyncCasesChecklist, + AsyncCasesChecklistWithRawResponse, + AsyncCasesChecklistWithStreamingResponse, + CasesChecklist, + CasesChecklistWithRawResponse, + CasesChecklistWithStreamingResponse, + ) + from didit.resources.cases.links import ( + AsyncCasesLinks, + AsyncCasesLinksWithRawResponse, + AsyncCasesLinksWithStreamingResponse, + CasesLinks, + CasesLinksWithRawResponse, + CasesLinksWithStreamingResponse, + ) + from didit.resources.cases.notes import ( + AsyncCasesNotes, + AsyncCasesNotesWithRawResponse, + AsyncCasesNotesWithStreamingResponse, + CasesNotes, + CasesNotesWithRawResponse, + CasesNotesWithStreamingResponse, + ) + from didit.resources.cases.regulatory_reports import ( + AsyncCasesRegulatoryReports, + AsyncCasesRegulatoryReportsWithRawResponse, + AsyncCasesRegulatoryReportsWithStreamingResponse, + CasesRegulatoryReports, + CasesRegulatoryReportsWithRawResponse, + CasesRegulatoryReportsWithStreamingResponse, + ) + from didit.resources.cases.requests import ( + AsyncCasesRequests, + AsyncCasesRequestsWithRawResponse, + AsyncCasesRequestsWithStreamingResponse, + CasesRequests, + CasesRequestsWithRawResponse, + CasesRequestsWithStreamingResponse, + ) + from didit.resources.cases.routing_rules import ( + AsyncCasesRoutingRules, + AsyncCasesRoutingRulesWithRawResponse, + AsyncCasesRoutingRulesWithStreamingResponse, + CasesRoutingRules, + CasesRoutingRulesWithRawResponse, + CasesRoutingRulesWithStreamingResponse, + ) + from didit.resources.cases.teams import ( + AsyncCasesTeams, + AsyncCasesTeamsWithRawResponse, + AsyncCasesTeamsWithStreamingResponse, + CasesTeams, + CasesTeamsWithRawResponse, + CasesTeamsWithStreamingResponse, + ) + +__all__ = [ + "Cases", + "AsyncCases", + "CasesWithRawResponse", + "AsyncCasesWithRawResponse", + "CasesWithStreamingResponse", + "AsyncCasesWithStreamingResponse", +] + + +class Cases(SyncAPIResource): + """The ``cases`` resource of the Didit API. + + Methods: ``apply_entity_action``, ``approve_resolution``, ``assign``, ``bulk_assign``, ``bulk_update_status``, ``create``, ``delete``, ``escalate``, ``find_duplicates``, ``list``, ``list_action_receipts``, ``list_aml_hits``, ``list_events``, ``list_reviewers``, ``print``, ``reject_resolution``, ``reopen``, ``request_info``, ``resolve``, ``retrieve``, ``retrieve_statistics``, ``retry_action_receipt``, ``transfer``, ``update``. + + Nested resources: ``blueprints``, ``checklist``, ``links``, ``notes``, ``regulatory_reports``, ``requests``, ``routing_rules``, ``teams``. + """ + + @cached_property + def blueprints(self) -> CasesBlueprints: + from didit.resources.cases.blueprints import CasesBlueprints + + return CasesBlueprints(self._client) + + @cached_property + def checklist(self) -> CasesChecklist: + from didit.resources.cases.checklist import CasesChecklist + + return CasesChecklist(self._client) + + @cached_property + def links(self) -> CasesLinks: + from didit.resources.cases.links import CasesLinks + + return CasesLinks(self._client) + + @cached_property + def notes(self) -> CasesNotes: + from didit.resources.cases.notes import CasesNotes + + return CasesNotes(self._client) + + @cached_property + def regulatory_reports(self) -> CasesRegulatoryReports: + from didit.resources.cases.regulatory_reports import CasesRegulatoryReports + + return CasesRegulatoryReports(self._client) + + @cached_property + def requests(self) -> CasesRequests: + from didit.resources.cases.requests import CasesRequests + + return CasesRequests(self._client) + + @cached_property + def routing_rules(self) -> CasesRoutingRules: + from didit.resources.cases.routing_rules import CasesRoutingRules + + return CasesRoutingRules(self._client) + + @cached_property + def teams(self) -> CasesTeams: + from didit.resources.cases.teams import CasesTeams + + return CasesTeams(self._client) + + @cached_property + def with_raw_response(self) -> CasesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status__in: str | NotGiven = NOT_GIVEN, + priority: str | NotGiven = NOT_GIVEN, + source__in: str | NotGiven = NOT_GIVEN, + blueprint: str | NotGiven = NOT_GIVEN, + assigned_to: str | NotGiven = NOT_GIVEN, + unassigned: bool | NotGiven = NOT_GIVEN, + tag: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + due_before: str | _dt.datetime | NotGiven = NOT_GIVEN, + overdue: bool | NotGiven = NOT_GIVEN, + ordering: Literal["created_at", "-created_at", "due_at", "-due_at"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CaseListItem]: + """List cases. + + List cases for the application, newest first. Supports filtering, search, and sorting; see query parameters. Paginated with `limit`/`offset`. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/`` + + Args: + limit: Page size. Defaults to 50. + offset: Zero-based offset of the first record. + status__in: Comma-separated statuses to include, e.g. OPEN,AWAITING_USER. + priority: Comma-separated priorities to include. + source__in: Comma-separated sources to include. + blueprint: Filter to cases on this blueprint. + assigned_to: Filter to cases assigned to this user. + unassigned: true for unassigned cases, false for assigned cases. + tag: Filter to cases carrying this exact tag. + search: Matches title, case_number, tag, subject name/UUID/external ID, linked session UUID, or linked transaction UUID. + date_from: Created-at lower bound (inclusive). + date_to: Created-at upper bound (inclusive). + due_before: due_at upper bound. + overdue: true to return only overdue open/awaiting-user cases. + ordering: Sort order. due_at sorts nulls last. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CaseListItem]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/", + cast_to=CasesListResponse, + item_type=CaseListItem, + scope="application", + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "status__in": status__in, + "priority": priority, + "source__in": source__in, + "blueprint": blueprint, + "assigned_to": assigned_to, + "unassigned": unassigned, + "tag": tag, + "search": search, + "date_from": date_from, + "date_to": date_to, + "due_before": due_before, + "overdue": overdue, + "ordering": ordering, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + title: str, + blueprint: str | None | NotGiven = NOT_GIVEN, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + due_at: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + subject: CasesCreateParamsSubject | NotGiven = NOT_GIVEN, + links: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + assigned_to: str | None | NotGiven = NOT_GIVEN, + review_duplicates: bool | NotGiven = NOT_GIVEN, + separate_incident_reason: str | NotGiven = NOT_GIVEN, + source_context: CaseDuplicateCaseFraudSourceContextRequest | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Create case. + + Create a case anchored to exactly one subject (a user or a business), directly or by deriving the subject from an attached session/transaction link. Source is always `MANUAL` for API-created cases. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/`` + + Args: + title + blueprint: CaseBlueprint UUID. Omit for no blueprint. + priority + due_at + tags + subject: Required unless links is provided; the case's exactly-one subject. + links: Sessions/business sessions/transactions to attach on creation, e.g. [{"entity_type": "transaction", "entity_uuid": "..."}]. When subject is omitted, the subject is derived from the first link's owner. + assigned_to: User UUID to assign immediately. + review_duplicates: Recheck existing investigations before creating the case. When true, a subject with existing cases requires a nonblank separate_incident_reason. The console always enables this protection. Omitting it preserves existing API behavior. + separate_incident_reason + source_context + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/", + cast_to=CaseDetail, + scope="application", + auth="api_key", + body={ + "title": title, + "blueprint": blueprint, + "priority": priority, + "due_at": due_at, + "tags": tags, + "subject": subject, + "links": links, + "assigned_to": assigned_to, + "review_duplicates": review_duplicates, + "separate_incident_reason": separate_incident_reason, + "source_context": source_context, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Get case. + + Retrieve a case with its subject, blueprint, notes, events, links, and checklist. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete case. + + Delete a case. There is no undelete endpoint. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/", + cast_to=None, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + case_uuid: str, + *, + title: str | NotGiven = NOT_GIVEN, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + due_at: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Update case. + + Update the case's title, priority, due date, or tags. Logs PRIORITY_CHANGED/DUE_DATE_CHANGED/TAG_ADDED/TAG_REMOVED events as applicable. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` + + Args: + case_uuid: Case UUID. + title + priority + due_at + tags + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"title": title, "priority": priority, "due_at": due_at, "tags": tags}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def assign( + self, + case_uuid: str, + *, + assignee_user_id: str | None, + assignee_email: str | None | NotGiven = NOT_GIVEN, + assignee_name: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Assign or unassign case. + + Assign the case to a user, or unassign it by sending assignee_user_id: null. assignee_email/assignee_name auto-provision a User row when the assignee does not exist yet. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/`` + + Args: + case_uuid: Case UUID. + assignee_user_id: null to unassign. + assignee_email + assignee_name + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "assignee_user_id": assignee_user_id, + "assignee_email": assignee_email, + "assignee_name": assignee_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def resolve( + self, + case_uuid: str, + *, + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str, + note: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Resolve case. + + Resolve the case as false_positive or valid_threat. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four_eyes_checker_blueprint configured, the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different user must call approve or reject-approval. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/`` + + Args: + case_uuid: Case UUID. + resolution + note: Required when the blueprint sets require_resolution_note. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"resolution": resolution, "note": note}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def reopen( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Reopen case. + + Move an awaiting-user or resolved case back to open, clearing resolution, resolution_notes, resolved_at, and resolved_by_email. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def request_info( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Request info (awaiting user). + + Move an open case to awaiting_user while more information is collected from the verified user. Fails with 400 if a resolution is currently pending 4-eyes approval. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def escalate( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Escalate case. + + Move the case to its blueprint's configured escalation_blueprint. Requires the current blueprint to have escalation_enabled and an escalation_blueprint set, otherwise 400. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def transfer( + self, + case_uuid: str, + *, + target_blueprint: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Transfer case. + + Move the case to a different, active blueprint of the same application. Requires the current blueprint to have transfer_enabled, otherwise 400. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/`` + + Args: + case_uuid: Case UUID. + target_blueprint + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"target_blueprint": target_blueprint}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def approve_resolution( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Approve staged resolution (4-eyes). + + Apply a resolution staged by resolve on a maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def reject_resolution( + self, + case_uuid: str, + *, + notes: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Reject staged resolution (4-eyes). + + Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/`` + + Args: + case_uuid: Case UUID. + notes + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"notes": notes}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def apply_entity_action( + self, + case_uuid: str, + *, + action: CaseActionActionEnum, + client_reference: str | NotGiven = NOT_GIVEN, + retry: bool | NotGiven = NOT_GIVEN, + status: str | None | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + note: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseActionCaseEntityActionResult: + """Apply one explicit action to the case subject. Supply a unique client_reference to obtain an action receipt and make retries idempotent. Reusing that reference with a different payload returns 409. With a reference, inspect receipt.status even when HTTP status is 200: FAILED means no action was applied. Repeating a failed request does not execute it again unless retry is true. Omitting client_reference preserves the legacy response. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/`` + + Args: + case_uuid + action: * `set_status` - set_status * `add_tags` - add_tags * `add_note` - add_note * `add_to_blocklist` - add_to_blocklist + client_reference + retry + status + tags + note + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseActionCaseEntityActionResult", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/", + cast_to=CaseActionCaseEntityActionResult, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "action": action, + "client_reference": client_reference, + "retry": retry, + "status": status, + "tags": tags, + "note": note, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def print( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesPrintResponse: + """Print case (PDF). + + Render the full case (summary, subject, checklist state, notes, attached sessions/transactions, events, FIU report list) as a PDF and return a presigned download URL. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesPrintResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/", + cast_to=CasesPrintResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_events( + self, + case_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CaseEvent]: + """List case events. + + Read-only audit timeline of the case (created, status changes, assignment, resolution, escalation, transfers, tag/priority/due-date changes, entity actions, and more). + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/`` + + Args: + case_uuid: Case UUID. + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CaseEvent]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/", + cast_to=CasesListEventsResponse, + item_type=CaseEvent, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_aml_hits( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[builtins.dict[str, object]]: + """List case AML hits. + + List AML screening hits for the case's subject. Same shape as the hits returned by POST /v3/aml/. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[builtins.dict[str, object]]", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/", + cast_to=builtins.list[builtins.dict[str, object]], + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_statistics( + self, + *, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + blueprint: str | NotGiven = NOT_GIVEN, + assigned_to: str | NotGiven = NOT_GIVEN, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + source: Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] + | str + | NotGiven = NOT_GIVEN, + status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesRetrieveStatisticsResponse: + """Case analytics. + + Aggregate case analytics for the date range (defaults to the last 30 days; max 731 days). Charts, tables, totals, assignment events, and current queue counts use the same application-scoped filters. Filters use current case values. Dates apply to each metric event, so older cases assigned or resolved during the period remain included. Queue depth is independent of the date range. Invalid filters return 400; an unknown blueprint or assignee returns an empty result. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/statistics/`` + + Args: + date_from: Range start (inclusive). Defaults to 30 days ago. + date_to: Range end (inclusive). Defaults to today. + blueprint: Filter by the current blueprint UUID. + assigned_to: Filter by the current assigned reviewer user UUID. + priority: Filter by current case priority. + source: Filter by case creation source. + status: Filter by current case status. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesRetrieveStatisticsResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/statistics/", + cast_to=CasesRetrieveStatisticsResponse, + scope="application", + auth="api_key", + query={ + "date_from": date_from, + "date_to": date_to, + "blueprint": blueprint, + "assigned_to": assigned_to, + "priority": priority, + "source": source, + "status": status, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def bulk_update_status( + self, + *, + case_uuids: Sequence[str], + status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str, + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | NotGiven = NOT_GIVEN, + note: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBulkUpdateStatusResponse: + """Bulk update case status. + + Apply the same status transition to several cases in one call: OPEN reopens, AWAITING_USER requests info, RESOLVED resolves (resolution required). Each case is evaluated independently; gate failures (incomplete checklist, missing required note) are reported per case without aborting the batch. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/`` + + Args: + case_uuids + status + resolution: Required when status is RESOLVED. + note + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBulkUpdateStatusResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/", + cast_to=CasesBulkUpdateStatusResponse, + scope="application", + auth="api_key", + body={ + "case_uuids": case_uuids, + "status": status, + "resolution": resolution, + "note": note, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def bulk_assign( + self, + *, + case_uuids: Sequence[str], + assignee_user_id: str | None, + assignee_email: str | None | NotGiven = NOT_GIVEN, + assignee_name: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBulkAssignResponse: + """Bulk assign cases. + + Assign (or unassign, with assignee_user_id: null) several cases at once. All cases must share the same blueprint; mismatches are reported per case without aborting the batch. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/`` + + Args: + case_uuids + assignee_user_id + assignee_email + assignee_name + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBulkAssignResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/", + cast_to=CasesBulkAssignResponse, + scope="application", + auth="api_key", + body={ + "case_uuids": case_uuids, + "assignee_user_id": assignee_user_id, + "assignee_email": assignee_email, + "assignee_name": assignee_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_action_receipts( + self, + case_uuid: str, + *, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseActionCaseActionReceiptPage: + """List subject-action receipts, newest first, with at most 20 results per page. Use next_offset for the next page. Input note text is not exposed in the history. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/`` + + Args: + case_uuid + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseActionCaseActionReceiptPage", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/", + cast_to=CaseActionCaseActionReceiptPage, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retry_action_receipt( + self, + case_uuid: str, + receipt_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseActionCaseActionReceipt: + """Retry only this failed subject action using its saved inputs. Current write permission is checked. Applied actions return their receipt without executing again. Case closure is independent. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/`` + + Args: + case_uuid + receipt_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseActionCaseActionReceipt", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/", + cast_to=CaseActionCaseActionReceipt, + path_params={"case_uuid": case_uuid, "receipt_uuid": receipt_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def find_duplicates( + self, + *, + subject: CaseDuplicateCaseSubjectInputRequest | NotGiven = NOT_GIVEN, + links: Sequence[CaseDuplicateCaseLinkCreateRequest] | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDuplicateCaseDuplicatePage: + """Review existing cases for a subject or owned source record before manual case creation. Both open and resolved investigations are returned. Each page contains at most 20 cases; use next_offset to request another page. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/duplicates/`` + + Args: + subject + links + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDuplicateCaseDuplicatePage", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/duplicates/", + cast_to=CaseDuplicateCaseDuplicatePage, + scope="application", + auth="api_key", + body={"subject": subject, "links": links, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_reviewers( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesListReviewersResponse: + """List eligible case reviewers. + + Requires `list:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/reviewers/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesListReviewersResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/reviewers/", + cast_to=CasesListReviewersResponse, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCases(AsyncAPIResource): + """The ``cases`` resource of the Didit API. + + Methods: ``apply_entity_action``, ``approve_resolution``, ``assign``, ``bulk_assign``, ``bulk_update_status``, ``create``, ``delete``, ``escalate``, ``find_duplicates``, ``list``, ``list_action_receipts``, ``list_aml_hits``, ``list_events``, ``list_reviewers``, ``print``, ``reject_resolution``, ``reopen``, ``request_info``, ``resolve``, ``retrieve``, ``retrieve_statistics``, ``retry_action_receipt``, ``transfer``, ``update``. + + Nested resources: ``blueprints``, ``checklist``, ``links``, ``notes``, ``regulatory_reports``, ``requests``, ``routing_rules``, ``teams``. + """ + + @cached_property + def blueprints(self) -> AsyncCasesBlueprints: + from didit.resources.cases.blueprints import AsyncCasesBlueprints + + return AsyncCasesBlueprints(self._client) + + @cached_property + def checklist(self) -> AsyncCasesChecklist: + from didit.resources.cases.checklist import AsyncCasesChecklist + + return AsyncCasesChecklist(self._client) + + @cached_property + def links(self) -> AsyncCasesLinks: + from didit.resources.cases.links import AsyncCasesLinks + + return AsyncCasesLinks(self._client) + + @cached_property + def notes(self) -> AsyncCasesNotes: + from didit.resources.cases.notes import AsyncCasesNotes + + return AsyncCasesNotes(self._client) + + @cached_property + def regulatory_reports(self) -> AsyncCasesRegulatoryReports: + from didit.resources.cases.regulatory_reports import AsyncCasesRegulatoryReports + + return AsyncCasesRegulatoryReports(self._client) + + @cached_property + def requests(self) -> AsyncCasesRequests: + from didit.resources.cases.requests import AsyncCasesRequests + + return AsyncCasesRequests(self._client) + + @cached_property + def routing_rules(self) -> AsyncCasesRoutingRules: + from didit.resources.cases.routing_rules import AsyncCasesRoutingRules + + return AsyncCasesRoutingRules(self._client) + + @cached_property + def teams(self) -> AsyncCasesTeams: + from didit.resources.cases.teams import AsyncCasesTeams + + return AsyncCasesTeams(self._client) + + @cached_property + def with_raw_response(self) -> AsyncCasesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status__in: str | NotGiven = NOT_GIVEN, + priority: str | NotGiven = NOT_GIVEN, + source__in: str | NotGiven = NOT_GIVEN, + blueprint: str | NotGiven = NOT_GIVEN, + assigned_to: str | NotGiven = NOT_GIVEN, + unassigned: bool | NotGiven = NOT_GIVEN, + tag: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + due_before: str | _dt.datetime | NotGiven = NOT_GIVEN, + overdue: bool | NotGiven = NOT_GIVEN, + ordering: Literal["created_at", "-created_at", "due_at", "-due_at"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CaseListItem]: + """List cases. + + List cases for the application, newest first. Supports filtering, search, and sorting; see query parameters. Paginated with `limit`/`offset`. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/`` + + Args: + limit: Page size. Defaults to 50. + offset: Zero-based offset of the first record. + status__in: Comma-separated statuses to include, e.g. OPEN,AWAITING_USER. + priority: Comma-separated priorities to include. + source__in: Comma-separated sources to include. + blueprint: Filter to cases on this blueprint. + assigned_to: Filter to cases assigned to this user. + unassigned: true for unassigned cases, false for assigned cases. + tag: Filter to cases carrying this exact tag. + search: Matches title, case_number, tag, subject name/UUID/external ID, linked session UUID, or linked transaction UUID. + date_from: Created-at lower bound (inclusive). + date_to: Created-at upper bound (inclusive). + due_before: due_at upper bound. + overdue: true to return only overdue open/awaiting-user cases. + ordering: Sort order. due_at sorts nulls last. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CaseListItem]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/", + cast_to=CasesListResponse, + item_type=CaseListItem, + scope="application", + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "status__in": status__in, + "priority": priority, + "source__in": source__in, + "blueprint": blueprint, + "assigned_to": assigned_to, + "unassigned": unassigned, + "tag": tag, + "search": search, + "date_from": date_from, + "date_to": date_to, + "due_before": due_before, + "overdue": overdue, + "ordering": ordering, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + title: str, + blueprint: str | None | NotGiven = NOT_GIVEN, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + due_at: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + subject: CasesCreateParamsSubject | NotGiven = NOT_GIVEN, + links: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + assigned_to: str | None | NotGiven = NOT_GIVEN, + review_duplicates: bool | NotGiven = NOT_GIVEN, + separate_incident_reason: str | NotGiven = NOT_GIVEN, + source_context: CaseDuplicateCaseFraudSourceContextRequest | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Create case. + + Create a case anchored to exactly one subject (a user or a business), directly or by deriving the subject from an attached session/transaction link. Source is always `MANUAL` for API-created cases. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/`` + + Args: + title + blueprint: CaseBlueprint UUID. Omit for no blueprint. + priority + due_at + tags + subject: Required unless links is provided; the case's exactly-one subject. + links: Sessions/business sessions/transactions to attach on creation, e.g. [{"entity_type": "transaction", "entity_uuid": "..."}]. When subject is omitted, the subject is derived from the first link's owner. + assigned_to: User UUID to assign immediately. + review_duplicates: Recheck existing investigations before creating the case. When true, a subject with existing cases requires a nonblank separate_incident_reason. The console always enables this protection. Omitting it preserves existing API behavior. + separate_incident_reason + source_context + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/", + cast_to=CaseDetail, + scope="application", + auth="api_key", + body={ + "title": title, + "blueprint": blueprint, + "priority": priority, + "due_at": due_at, + "tags": tags, + "subject": subject, + "links": links, + "assigned_to": assigned_to, + "review_duplicates": review_duplicates, + "separate_incident_reason": separate_incident_reason, + "source_context": source_context, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Get case. + + Retrieve a case with its subject, blueprint, notes, events, links, and checklist. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete case. + + Delete a case. There is no undelete endpoint. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/", + cast_to=None, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + case_uuid: str, + *, + title: str | NotGiven = NOT_GIVEN, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + due_at: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Update case. + + Update the case's title, priority, due date, or tags. Logs PRIORITY_CHANGED/DUE_DATE_CHANGED/TAG_ADDED/TAG_REMOVED events as applicable. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/`` + + Args: + case_uuid: Case UUID. + title + priority + due_at + tags + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"title": title, "priority": priority, "due_at": due_at, "tags": tags}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def assign( + self, + case_uuid: str, + *, + assignee_user_id: str | None, + assignee_email: str | None | NotGiven = NOT_GIVEN, + assignee_name: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Assign or unassign case. + + Assign the case to a user, or unassign it by sending assignee_user_id: null. assignee_email/assignee_name auto-provision a User row when the assignee does not exist yet. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/`` + + Args: + case_uuid: Case UUID. + assignee_user_id: null to unassign. + assignee_email + assignee_name + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "assignee_user_id": assignee_user_id, + "assignee_email": assignee_email, + "assignee_name": assignee_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def resolve( + self, + case_uuid: str, + *, + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str, + note: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Resolve case. + + Resolve the case as false_positive or valid_threat. Blocked with 400 if the blueprint's checklist is not optional and items remain incomplete, or if the blueprint requires a resolution note and none is given. When the case's blueprint has a four_eyes_checker_blueprint configured, the resolution is staged (case.pending_resolution) and the case moves to the checker blueprint instead of resolving immediately; a different user must call approve or reject-approval. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/`` + + Args: + case_uuid: Case UUID. + resolution + note: Required when the blueprint sets require_resolution_note. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"resolution": resolution, "note": note}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def reopen( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Reopen case. + + Move an awaiting-user or resolved case back to open, clearing resolution, resolution_notes, resolved_at, and resolved_by_email. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def request_info( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Request info (awaiting user). + + Move an open case to awaiting_user while more information is collected from the verified user. Fails with 400 if a resolution is currently pending 4-eyes approval. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def escalate( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Escalate case. + + Move the case to its blueprint's configured escalation_blueprint. Requires the current blueprint to have escalation_enabled and an escalation_blueprint set, otherwise 400. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def transfer( + self, + case_uuid: str, + *, + target_blueprint: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Transfer case. + + Move the case to a different, active blueprint of the same application. Requires the current blueprint to have transfer_enabled, otherwise 400. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/`` + + Args: + case_uuid: Case UUID. + target_blueprint + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"target_blueprint": target_blueprint}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def approve_resolution( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Approve staged resolution (4-eyes). + + Apply a resolution staged by resolve on a maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def reject_resolution( + self, + case_uuid: str, + *, + notes: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDetail: + """Reject staged resolution (4-eyes). + + Reject a resolution staged by resolve and return the case to its maker blueprint. The caller must be a different user than the one who submitted it, otherwise 403. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/`` + + Args: + case_uuid: Case UUID. + notes + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/", + cast_to=CaseDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"notes": notes}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def apply_entity_action( + self, + case_uuid: str, + *, + action: CaseActionActionEnum, + client_reference: str | NotGiven = NOT_GIVEN, + retry: bool | NotGiven = NOT_GIVEN, + status: str | None | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + note: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseActionCaseEntityActionResult: + """Apply one explicit action to the case subject. Supply a unique client_reference to obtain an action receipt and make retries idempotent. Reusing that reference with a different payload returns 409. With a reference, inspect receipt.status even when HTTP status is 200: FAILED means no action was applied. Repeating a failed request does not execute it again unless retry is true. Omitting client_reference preserves the legacy response. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/`` + + Args: + case_uuid + action: * `set_status` - set_status * `add_tags` - add_tags * `add_note` - add_note * `add_to_blocklist` - add_to_blocklist + client_reference + retry + status + tags + note + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseActionCaseEntityActionResult", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/", + cast_to=CaseActionCaseEntityActionResult, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "action": action, + "client_reference": client_reference, + "retry": retry, + "status": status, + "tags": tags, + "note": note, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def print( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesPrintResponse: + """Print case (PDF). + + Render the full case (summary, subject, checklist state, notes, attached sessions/transactions, events, FIU report list) as a PDF and return a presigned download URL. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesPrintResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/", + cast_to=CasesPrintResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_events( + self, + case_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CaseEvent]: + """List case events. + + Read-only audit timeline of the case (created, status changes, assignment, resolution, escalation, transfers, tag/priority/due-date changes, entity actions, and more). + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/`` + + Args: + case_uuid: Case UUID. + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CaseEvent]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/", + cast_to=CasesListEventsResponse, + item_type=CaseEvent, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_aml_hits( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[builtins.dict[str, object]]: + """List case AML hits. + + List AML screening hits for the case's subject. Same shape as the hits returned by POST /v3/aml/. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[builtins.dict[str, object]]", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/", + cast_to=builtins.list[builtins.dict[str, object]], + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_statistics( + self, + *, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + blueprint: str | NotGiven = NOT_GIVEN, + assigned_to: str | NotGiven = NOT_GIVEN, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + source: Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] + | str + | NotGiven = NOT_GIVEN, + status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesRetrieveStatisticsResponse: + """Case analytics. + + Aggregate case analytics for the date range (defaults to the last 30 days; max 731 days). Charts, tables, totals, assignment events, and current queue counts use the same application-scoped filters. Filters use current case values. Dates apply to each metric event, so older cases assigned or resolved during the period remain included. Queue depth is independent of the date range. Invalid filters return 400; an unknown blueprint or assignee returns an empty result. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/statistics/`` + + Args: + date_from: Range start (inclusive). Defaults to 30 days ago. + date_to: Range end (inclusive). Defaults to today. + blueprint: Filter by the current blueprint UUID. + assigned_to: Filter by the current assigned reviewer user UUID. + priority: Filter by current case priority. + source: Filter by case creation source. + status: Filter by current case status. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesRetrieveStatisticsResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/statistics/", + cast_to=CasesRetrieveStatisticsResponse, + scope="application", + auth="api_key", + query={ + "date_from": date_from, + "date_to": date_to, + "blueprint": blueprint, + "assigned_to": assigned_to, + "priority": priority, + "source": source, + "status": status, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def bulk_update_status( + self, + *, + case_uuids: Sequence[str], + status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str, + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | NotGiven = NOT_GIVEN, + note: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBulkUpdateStatusResponse: + """Bulk update case status. + + Apply the same status transition to several cases in one call: OPEN reopens, AWAITING_USER requests info, RESOLVED resolves (resolution required). Each case is evaluated independently; gate failures (incomplete checklist, missing required note) are reported per case without aborting the batch. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/`` + + Args: + case_uuids + status + resolution: Required when status is RESOLVED. + note + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBulkUpdateStatusResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/", + cast_to=CasesBulkUpdateStatusResponse, + scope="application", + auth="api_key", + body={ + "case_uuids": case_uuids, + "status": status, + "resolution": resolution, + "note": note, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def bulk_assign( + self, + *, + case_uuids: Sequence[str], + assignee_user_id: str | None, + assignee_email: str | None | NotGiven = NOT_GIVEN, + assignee_name: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBulkAssignResponse: + """Bulk assign cases. + + Assign (or unassign, with assignee_user_id: null) several cases at once. All cases must share the same blueprint; mismatches are reported per case without aborting the batch. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/`` + + Args: + case_uuids + assignee_user_id + assignee_email + assignee_name + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBulkAssignResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/", + cast_to=CasesBulkAssignResponse, + scope="application", + auth="api_key", + body={ + "case_uuids": case_uuids, + "assignee_user_id": assignee_user_id, + "assignee_email": assignee_email, + "assignee_name": assignee_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_action_receipts( + self, + case_uuid: str, + *, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseActionCaseActionReceiptPage: + """List subject-action receipts, newest first, with at most 20 results per page. Use next_offset for the next page. Input note text is not exposed in the history. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/`` + + Args: + case_uuid + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseActionCaseActionReceiptPage", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/", + cast_to=CaseActionCaseActionReceiptPage, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retry_action_receipt( + self, + case_uuid: str, + receipt_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseActionCaseActionReceipt: + """Retry only this failed subject action using its saved inputs. Current write permission is checked. Applied actions return their receipt without executing again. Case closure is independent. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/`` + + Args: + case_uuid + receipt_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseActionCaseActionReceipt", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/", + cast_to=CaseActionCaseActionReceipt, + path_params={"case_uuid": case_uuid, "receipt_uuid": receipt_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def find_duplicates( + self, + *, + subject: CaseDuplicateCaseSubjectInputRequest | NotGiven = NOT_GIVEN, + links: Sequence[CaseDuplicateCaseLinkCreateRequest] | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseDuplicateCaseDuplicatePage: + """Review existing cases for a subject or owned source record before manual case creation. Both open and resolved investigations are returned. Each page contains at most 20 cases; use next_offset to request another page. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/duplicates/`` + + Args: + subject + links + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseDuplicateCaseDuplicatePage", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/duplicates/", + cast_to=CaseDuplicateCaseDuplicatePage, + scope="application", + auth="api_key", + body={"subject": subject, "links": links, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_reviewers( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesListReviewersResponse: + """List eligible case reviewers. + + Requires `list:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/reviewers/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesListReviewersResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/reviewers/", + cast_to=CasesListReviewersResponse, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesWithRawResponse: + def __init__(self, resource: Cases) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + self.assign = to_raw_response_wrapper(resource.assign) + self.resolve = to_raw_response_wrapper(resource.resolve) + self.reopen = to_raw_response_wrapper(resource.reopen) + self.request_info = to_raw_response_wrapper(resource.request_info) + self.escalate = to_raw_response_wrapper(resource.escalate) + self.transfer = to_raw_response_wrapper(resource.transfer) + self.approve_resolution = to_raw_response_wrapper(resource.approve_resolution) + self.reject_resolution = to_raw_response_wrapper(resource.reject_resolution) + self.apply_entity_action = to_raw_response_wrapper(resource.apply_entity_action) + self.print = to_raw_response_wrapper(resource.print) + self.list_events = to_raw_response_wrapper(resource.list_events) + self.list_aml_hits = to_raw_response_wrapper(resource.list_aml_hits) + self.retrieve_statistics = to_raw_response_wrapper(resource.retrieve_statistics) + self.bulk_update_status = to_raw_response_wrapper(resource.bulk_update_status) + self.bulk_assign = to_raw_response_wrapper(resource.bulk_assign) + self.list_action_receipts = to_raw_response_wrapper(resource.list_action_receipts) + self.retry_action_receipt = to_raw_response_wrapper(resource.retry_action_receipt) + self.find_duplicates = to_raw_response_wrapper(resource.find_duplicates) + self.list_reviewers = to_raw_response_wrapper(resource.list_reviewers) + + @cached_property + def blueprints(self) -> CasesBlueprintsWithRawResponse: + from didit.resources.cases.blueprints import CasesBlueprintsWithRawResponse + + return CasesBlueprintsWithRawResponse(self._resource.blueprints) + + @cached_property + def checklist(self) -> CasesChecklistWithRawResponse: + from didit.resources.cases.checklist import CasesChecklistWithRawResponse + + return CasesChecklistWithRawResponse(self._resource.checklist) + + @cached_property + def links(self) -> CasesLinksWithRawResponse: + from didit.resources.cases.links import CasesLinksWithRawResponse + + return CasesLinksWithRawResponse(self._resource.links) + + @cached_property + def notes(self) -> CasesNotesWithRawResponse: + from didit.resources.cases.notes import CasesNotesWithRawResponse + + return CasesNotesWithRawResponse(self._resource.notes) + + @cached_property + def regulatory_reports(self) -> CasesRegulatoryReportsWithRawResponse: + from didit.resources.cases.regulatory_reports import CasesRegulatoryReportsWithRawResponse + + return CasesRegulatoryReportsWithRawResponse(self._resource.regulatory_reports) + + @cached_property + def requests(self) -> CasesRequestsWithRawResponse: + from didit.resources.cases.requests import CasesRequestsWithRawResponse + + return CasesRequestsWithRawResponse(self._resource.requests) + + @cached_property + def routing_rules(self) -> CasesRoutingRulesWithRawResponse: + from didit.resources.cases.routing_rules import CasesRoutingRulesWithRawResponse + + return CasesRoutingRulesWithRawResponse(self._resource.routing_rules) + + @cached_property + def teams(self) -> CasesTeamsWithRawResponse: + from didit.resources.cases.teams import CasesTeamsWithRawResponse + + return CasesTeamsWithRawResponse(self._resource.teams) + + +class AsyncCasesWithRawResponse: + def __init__(self, resource: AsyncCases) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + self.assign = async_to_raw_response_wrapper(resource.assign) + self.resolve = async_to_raw_response_wrapper(resource.resolve) + self.reopen = async_to_raw_response_wrapper(resource.reopen) + self.request_info = async_to_raw_response_wrapper(resource.request_info) + self.escalate = async_to_raw_response_wrapper(resource.escalate) + self.transfer = async_to_raw_response_wrapper(resource.transfer) + self.approve_resolution = async_to_raw_response_wrapper(resource.approve_resolution) + self.reject_resolution = async_to_raw_response_wrapper(resource.reject_resolution) + self.apply_entity_action = async_to_raw_response_wrapper(resource.apply_entity_action) + self.print = async_to_raw_response_wrapper(resource.print) + self.list_events = async_to_raw_response_wrapper(resource.list_events) + self.list_aml_hits = async_to_raw_response_wrapper(resource.list_aml_hits) + self.retrieve_statistics = async_to_raw_response_wrapper(resource.retrieve_statistics) + self.bulk_update_status = async_to_raw_response_wrapper(resource.bulk_update_status) + self.bulk_assign = async_to_raw_response_wrapper(resource.bulk_assign) + self.list_action_receipts = async_to_raw_response_wrapper(resource.list_action_receipts) + self.retry_action_receipt = async_to_raw_response_wrapper(resource.retry_action_receipt) + self.find_duplicates = async_to_raw_response_wrapper(resource.find_duplicates) + self.list_reviewers = async_to_raw_response_wrapper(resource.list_reviewers) + + @cached_property + def blueprints(self) -> AsyncCasesBlueprintsWithRawResponse: + from didit.resources.cases.blueprints import AsyncCasesBlueprintsWithRawResponse + + return AsyncCasesBlueprintsWithRawResponse(self._resource.blueprints) + + @cached_property + def checklist(self) -> AsyncCasesChecklistWithRawResponse: + from didit.resources.cases.checklist import AsyncCasesChecklistWithRawResponse + + return AsyncCasesChecklistWithRawResponse(self._resource.checklist) + + @cached_property + def links(self) -> AsyncCasesLinksWithRawResponse: + from didit.resources.cases.links import AsyncCasesLinksWithRawResponse + + return AsyncCasesLinksWithRawResponse(self._resource.links) + + @cached_property + def notes(self) -> AsyncCasesNotesWithRawResponse: + from didit.resources.cases.notes import AsyncCasesNotesWithRawResponse + + return AsyncCasesNotesWithRawResponse(self._resource.notes) + + @cached_property + def regulatory_reports(self) -> AsyncCasesRegulatoryReportsWithRawResponse: + from didit.resources.cases.regulatory_reports import ( + AsyncCasesRegulatoryReportsWithRawResponse, + ) + + return AsyncCasesRegulatoryReportsWithRawResponse(self._resource.regulatory_reports) + + @cached_property + def requests(self) -> AsyncCasesRequestsWithRawResponse: + from didit.resources.cases.requests import AsyncCasesRequestsWithRawResponse + + return AsyncCasesRequestsWithRawResponse(self._resource.requests) + + @cached_property + def routing_rules(self) -> AsyncCasesRoutingRulesWithRawResponse: + from didit.resources.cases.routing_rules import AsyncCasesRoutingRulesWithRawResponse + + return AsyncCasesRoutingRulesWithRawResponse(self._resource.routing_rules) + + @cached_property + def teams(self) -> AsyncCasesTeamsWithRawResponse: + from didit.resources.cases.teams import AsyncCasesTeamsWithRawResponse + + return AsyncCasesTeamsWithRawResponse(self._resource.teams) + + +class CasesWithStreamingResponse: + def __init__(self, resource: Cases) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + self.assign = to_streamed_response_wrapper(resource.assign) + self.resolve = to_streamed_response_wrapper(resource.resolve) + self.reopen = to_streamed_response_wrapper(resource.reopen) + self.request_info = to_streamed_response_wrapper(resource.request_info) + self.escalate = to_streamed_response_wrapper(resource.escalate) + self.transfer = to_streamed_response_wrapper(resource.transfer) + self.approve_resolution = to_streamed_response_wrapper(resource.approve_resolution) + self.reject_resolution = to_streamed_response_wrapper(resource.reject_resolution) + self.apply_entity_action = to_streamed_response_wrapper(resource.apply_entity_action) + self.print = to_streamed_response_wrapper(resource.print) + self.list_events = to_streamed_response_wrapper(resource.list_events) + self.list_aml_hits = to_streamed_response_wrapper(resource.list_aml_hits) + self.retrieve_statistics = to_streamed_response_wrapper(resource.retrieve_statistics) + self.bulk_update_status = to_streamed_response_wrapper(resource.bulk_update_status) + self.bulk_assign = to_streamed_response_wrapper(resource.bulk_assign) + self.list_action_receipts = to_streamed_response_wrapper(resource.list_action_receipts) + self.retry_action_receipt = to_streamed_response_wrapper(resource.retry_action_receipt) + self.find_duplicates = to_streamed_response_wrapper(resource.find_duplicates) + self.list_reviewers = to_streamed_response_wrapper(resource.list_reviewers) + + @cached_property + def blueprints(self) -> CasesBlueprintsWithStreamingResponse: + from didit.resources.cases.blueprints import CasesBlueprintsWithStreamingResponse + + return CasesBlueprintsWithStreamingResponse(self._resource.blueprints) + + @cached_property + def checklist(self) -> CasesChecklistWithStreamingResponse: + from didit.resources.cases.checklist import CasesChecklistWithStreamingResponse + + return CasesChecklistWithStreamingResponse(self._resource.checklist) + + @cached_property + def links(self) -> CasesLinksWithStreamingResponse: + from didit.resources.cases.links import CasesLinksWithStreamingResponse + + return CasesLinksWithStreamingResponse(self._resource.links) + + @cached_property + def notes(self) -> CasesNotesWithStreamingResponse: + from didit.resources.cases.notes import CasesNotesWithStreamingResponse + + return CasesNotesWithStreamingResponse(self._resource.notes) + + @cached_property + def regulatory_reports(self) -> CasesRegulatoryReportsWithStreamingResponse: + from didit.resources.cases.regulatory_reports import ( + CasesRegulatoryReportsWithStreamingResponse, + ) + + return CasesRegulatoryReportsWithStreamingResponse(self._resource.regulatory_reports) + + @cached_property + def requests(self) -> CasesRequestsWithStreamingResponse: + from didit.resources.cases.requests import CasesRequestsWithStreamingResponse + + return CasesRequestsWithStreamingResponse(self._resource.requests) + + @cached_property + def routing_rules(self) -> CasesRoutingRulesWithStreamingResponse: + from didit.resources.cases.routing_rules import CasesRoutingRulesWithStreamingResponse + + return CasesRoutingRulesWithStreamingResponse(self._resource.routing_rules) + + @cached_property + def teams(self) -> CasesTeamsWithStreamingResponse: + from didit.resources.cases.teams import CasesTeamsWithStreamingResponse + + return CasesTeamsWithStreamingResponse(self._resource.teams) + + +class AsyncCasesWithStreamingResponse: + def __init__(self, resource: AsyncCases) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) + self.assign = async_to_streamed_response_wrapper(resource.assign) + self.resolve = async_to_streamed_response_wrapper(resource.resolve) + self.reopen = async_to_streamed_response_wrapper(resource.reopen) + self.request_info = async_to_streamed_response_wrapper(resource.request_info) + self.escalate = async_to_streamed_response_wrapper(resource.escalate) + self.transfer = async_to_streamed_response_wrapper(resource.transfer) + self.approve_resolution = async_to_streamed_response_wrapper(resource.approve_resolution) + self.reject_resolution = async_to_streamed_response_wrapper(resource.reject_resolution) + self.apply_entity_action = async_to_streamed_response_wrapper(resource.apply_entity_action) + self.print = async_to_streamed_response_wrapper(resource.print) + self.list_events = async_to_streamed_response_wrapper(resource.list_events) + self.list_aml_hits = async_to_streamed_response_wrapper(resource.list_aml_hits) + self.retrieve_statistics = async_to_streamed_response_wrapper(resource.retrieve_statistics) + self.bulk_update_status = async_to_streamed_response_wrapper(resource.bulk_update_status) + self.bulk_assign = async_to_streamed_response_wrapper(resource.bulk_assign) + self.list_action_receipts = async_to_streamed_response_wrapper( + resource.list_action_receipts + ) + self.retry_action_receipt = async_to_streamed_response_wrapper( + resource.retry_action_receipt + ) + self.find_duplicates = async_to_streamed_response_wrapper(resource.find_duplicates) + self.list_reviewers = async_to_streamed_response_wrapper(resource.list_reviewers) + + @cached_property + def blueprints(self) -> AsyncCasesBlueprintsWithStreamingResponse: + from didit.resources.cases.blueprints import AsyncCasesBlueprintsWithStreamingResponse + + return AsyncCasesBlueprintsWithStreamingResponse(self._resource.blueprints) + + @cached_property + def checklist(self) -> AsyncCasesChecklistWithStreamingResponse: + from didit.resources.cases.checklist import AsyncCasesChecklistWithStreamingResponse + + return AsyncCasesChecklistWithStreamingResponse(self._resource.checklist) + + @cached_property + def links(self) -> AsyncCasesLinksWithStreamingResponse: + from didit.resources.cases.links import AsyncCasesLinksWithStreamingResponse + + return AsyncCasesLinksWithStreamingResponse(self._resource.links) + + @cached_property + def notes(self) -> AsyncCasesNotesWithStreamingResponse: + from didit.resources.cases.notes import AsyncCasesNotesWithStreamingResponse + + return AsyncCasesNotesWithStreamingResponse(self._resource.notes) + + @cached_property + def regulatory_reports(self) -> AsyncCasesRegulatoryReportsWithStreamingResponse: + from didit.resources.cases.regulatory_reports import ( + AsyncCasesRegulatoryReportsWithStreamingResponse, + ) + + return AsyncCasesRegulatoryReportsWithStreamingResponse(self._resource.regulatory_reports) + + @cached_property + def requests(self) -> AsyncCasesRequestsWithStreamingResponse: + from didit.resources.cases.requests import AsyncCasesRequestsWithStreamingResponse + + return AsyncCasesRequestsWithStreamingResponse(self._resource.requests) + + @cached_property + def routing_rules(self) -> AsyncCasesRoutingRulesWithStreamingResponse: + from didit.resources.cases.routing_rules import AsyncCasesRoutingRulesWithStreamingResponse + + return AsyncCasesRoutingRulesWithStreamingResponse(self._resource.routing_rules) + + @cached_property + def teams(self) -> AsyncCasesTeamsWithStreamingResponse: + from didit.resources.cases.teams import AsyncCasesTeamsWithStreamingResponse + + return AsyncCasesTeamsWithStreamingResponse(self._resource.teams) diff --git a/src/didit/resources/cases/blueprints.py b/src/didit/resources/cases/blueprints.py new file mode 100644 index 0000000..3613ea2 --- /dev/null +++ b/src/didit/resources/cases/blueprints.py @@ -0,0 +1,1247 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.blueprints`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.cases.blueprints.install_presets_response import ( + CasesBlueprintsInstallPresetsResponse, +) +from didit.types.cases.blueprints.list_presets_response import CasesBlueprintsListPresetsResponse +from didit.types.cases.blueprints.list_response import CasesBlueprintsListResponse +from didit.types.components.case_blueprint_detail import CaseBlueprintDetail +from didit.types.components.case_blueprint_list_item import CaseBlueprintListItem +from didit.types.components.case_content_config import CaseContentConfigParam +from didit.types.components.case_publication_case_blueprint import CasePublicationCaseBlueprint +from didit.types.components.case_publication_paginated_policy_revision_list import ( + CasePublicationPaginatedPolicyRevisionList, +) +from didit.types.components.case_publication_policy_draft import CasePublicationPolicyDraft +from didit.types.components.case_publication_policy_draft_response import ( + CasePublicationPolicyDraftResponse, +) +from didit.types.components.case_publication_policy_revision import CasePublicationPolicyRevision + +__all__ = [ + "CasesBlueprints", + "AsyncCasesBlueprints", + "CasesBlueprintsWithRawResponse", + "AsyncCasesBlueprintsWithRawResponse", + "CasesBlueprintsWithStreamingResponse", + "AsyncCasesBlueprintsWithStreamingResponse", +] + + +class CasesBlueprints(SyncAPIResource): + """The ``cases.blueprints`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``discard_draft``, ``install_presets``, ``list``, ``list_history``, ``list_presets``, ``publish``, ``retrieve``, ``retrieve_draft``, ``save_draft``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> CasesBlueprintsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesBlueprintsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesBlueprintsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesBlueprintsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CaseBlueprintListItem]: + """List blueprints. + + List case blueprints for the application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/`` + + Args: + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CaseBlueprintListItem]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/", + cast_to=CasesBlueprintsListResponse, + item_type=CaseBlueprintListItem, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + name: str, + assignee_pool: Sequence[str], + description: str | None | NotGiven = NOT_GIVEN, + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str | NotGiven = NOT_GIVEN, + max_active_cases: int | None | NotGiven = NOT_GIVEN, + escalation_enabled: bool | NotGiven = NOT_GIVEN, + escalation_blueprint: str | None | NotGiven = NOT_GIVEN, + transfer_enabled: bool | NotGiven = NOT_GIVEN, + four_eyes_checker_blueprint: str | None | NotGiven = NOT_GIVEN, + require_resolution_note: bool | NotGiven = NOT_GIVEN, + content_config: CaseContentConfigParam | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + default_deadline_hours: int | None | NotGiven = NOT_GIVEN, + publish: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseBlueprintDetail: + """Create blueprint. + + Create a case blueprint: general settings (assignment, escalation, transfer, 4-eyes pairing) plus content_config, the six sections that drive the case page. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/`` + + Args: + name: Max 24 characters, e.g. "AML investigation". + assignee_pool: List of User UUIDs. Empty arrays are rejected with "Pick at least one assignee". + description + assignment_method + max_active_cases: Automatic-mode threshold: "fewer than N cases of this blueprint assigned". + escalation_enabled + escalation_blueprint: Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set. + transfer_enabled + four_eyes_checker_blueprint: Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue. + require_resolution_note + content_config: The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected. + is_active + default_deadline_hours: Default deadline for newly opened cases. Existing case deadlines are unchanged. + publish: Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseBlueprintDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/", + cast_to=CaseBlueprintDetail, + scope="application", + auth="api_key", + body={ + "name": name, + "assignee_pool": assignee_pool, + "description": description, + "assignment_method": assignment_method, + "max_active_cases": max_active_cases, + "escalation_enabled": escalation_enabled, + "escalation_blueprint": escalation_blueprint, + "transfer_enabled": transfer_enabled, + "four_eyes_checker_blueprint": four_eyes_checker_blueprint, + "require_resolution_note": require_resolution_note, + "content_config": content_config, + "is_active": is_active, + "default_deadline_hours": default_deadline_hours, + "publish": publish, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + blueprint_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseBlueprintDetail: + """Get blueprint. + + Retrieve a blueprint, including its connected creation sources (AML config, transaction rules, workflow nodes) and which blueprints escalate to it. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/`` + + Args: + blueprint_uuid: Blueprint UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseBlueprintDetail", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/", + cast_to=CaseBlueprintDetail, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + blueprint_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete blueprint. + + Delete a blueprint. Deletion always succeeds and does not touch the blueprint's cases: existing cases keep their blueprint reference and its settings continue to apply to them. A deleted blueprint no longer appears in blueprint lists and can no longer be chosen as a transfer target; if a maker blueprint is deleted while a resolution is pending 4-eyes approval, a rejected resolution leaves the case on the checker blueprint. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/`` + + Args: + blueprint_uuid: Blueprint UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/", + cast_to=None, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + blueprint_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | None | NotGiven = NOT_GIVEN, + assignee_pool: Sequence[str] | NotGiven = NOT_GIVEN, + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str | NotGiven = NOT_GIVEN, + max_active_cases: int | None | NotGiven = NOT_GIVEN, + escalation_enabled: bool | NotGiven = NOT_GIVEN, + escalation_blueprint: str | None | NotGiven = NOT_GIVEN, + transfer_enabled: bool | NotGiven = NOT_GIVEN, + four_eyes_checker_blueprint: str | None | NotGiven = NOT_GIVEN, + require_resolution_note: bool | NotGiven = NOT_GIVEN, + content_config: CaseContentConfigParam | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + default_deadline_hours: int | None | NotGiven = NOT_GIVEN, + expected_version: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseBlueprintDetail: + """Update blueprint. + + Partially update a blueprint's general settings or content_config. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/`` + + Args: + blueprint_uuid: Blueprint UUID. + name: Max 24 characters, e.g. "AML investigation". + description + assignee_pool: List of User UUIDs. Empty arrays are rejected with "Pick at least one assignee". + assignment_method + max_active_cases: Automatic-mode threshold: "fewer than N cases of this blueprint assigned". + escalation_enabled + escalation_blueprint: Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set. + transfer_enabled + four_eyes_checker_blueprint: Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue. + require_resolution_note + content_config: The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected. + is_active + default_deadline_hours: Default deadline for newly opened cases. Existing case deadlines are unchanged. + expected_version: The version opened by the editor. A stale version returns 409 without changing the review policy. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseBlueprintDetail", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/", + cast_to=CaseBlueprintDetail, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "description": description, + "assignee_pool": assignee_pool, + "assignment_method": assignment_method, + "max_active_cases": max_active_cases, + "escalation_enabled": escalation_enabled, + "escalation_blueprint": escalation_blueprint, + "transfer_enabled": transfer_enabled, + "four_eyes_checker_blueprint": four_eyes_checker_blueprint, + "require_resolution_note": require_resolution_note, + "content_config": content_config, + "is_active": is_active, + "default_deadline_hours": default_deadline_hours, + "expected_version": expected_version, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_presets( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBlueprintsListPresetsResponse: + """List blueprint presets. + + List the four built-in presets (Screening hit, AML investigation, Suspicious activity, KYC review) with their content_config and whether each is already installed on this application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBlueprintsListPresetsResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/", + cast_to=CasesBlueprintsListPresetsResponse, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def install_presets( + self, + *, + preset_keys: Sequence[str] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBlueprintsInstallPresetsResponse: + """Install blueprint presets. + + Copy one or more presets into the application as editable blueprints. Idempotent per preset_key; omit preset_keys to install every preset not yet installed. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/`` + + Args: + preset_keys: Subset of screening_hit, aml_investigation, suspicious_activity, kyc. Omit to install all. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBlueprintsInstallPresetsResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/", + cast_to=CasesBlueprintsInstallPresetsResponse, + scope="application", + auth="api_key", + body={"preset_keys": preset_keys}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def publish( + self, + blueprint_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasePublicationCaseBlueprint: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/`` + + Args: + blueprint_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasePublicationCaseBlueprint", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/", + cast_to=CasePublicationCaseBlueprint, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_draft( + self, + blueprint_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasePublicationPolicyDraftResponse: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/`` + + Args: + blueprint_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasePublicationPolicyDraftResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/", + cast_to=CasePublicationPolicyDraftResponse, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def save_draft( + self, + blueprint_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + changes: Mapping[str, object], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasePublicationPolicyDraft: + """``PUT /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/`` + + Args: + blueprint_uuid + expected_version + expected_revision + changes + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasePublicationPolicyDraft", + self._client.request( + "put", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/", + cast_to=CasePublicationPolicyDraft, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={ + "expected_version": expected_version, + "expected_revision": expected_revision, + "changes": changes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def discard_draft( + self, + blueprint_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/`` + + Args: + blueprint_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/", + cast_to=None, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_history( + self, + blueprint_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CasePublicationPolicyRevision]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/`` + + Args: + blueprint_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CasePublicationPolicyRevision]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/", + cast_to=CasePublicationPaginatedPolicyRevisionList, + item_type=CasePublicationPolicyRevision, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesBlueprints(AsyncAPIResource): + """The ``cases.blueprints`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``discard_draft``, ``install_presets``, ``list``, ``list_history``, ``list_presets``, ``publish``, ``retrieve``, ``retrieve_draft``, ``save_draft``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesBlueprintsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesBlueprintsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesBlueprintsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesBlueprintsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CaseBlueprintListItem]: + """List blueprints. + + List case blueprints for the application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/`` + + Args: + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CaseBlueprintListItem]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/", + cast_to=CasesBlueprintsListResponse, + item_type=CaseBlueprintListItem, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + name: str, + assignee_pool: Sequence[str], + description: str | None | NotGiven = NOT_GIVEN, + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str | NotGiven = NOT_GIVEN, + max_active_cases: int | None | NotGiven = NOT_GIVEN, + escalation_enabled: bool | NotGiven = NOT_GIVEN, + escalation_blueprint: str | None | NotGiven = NOT_GIVEN, + transfer_enabled: bool | NotGiven = NOT_GIVEN, + four_eyes_checker_blueprint: str | None | NotGiven = NOT_GIVEN, + require_resolution_note: bool | NotGiven = NOT_GIVEN, + content_config: CaseContentConfigParam | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + default_deadline_hours: int | None | NotGiven = NOT_GIVEN, + publish: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseBlueprintDetail: + """Create blueprint. + + Create a case blueprint: general settings (assignment, escalation, transfer, 4-eyes pairing) plus content_config, the six sections that drive the case page. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/`` + + Args: + name: Max 24 characters, e.g. "AML investigation". + assignee_pool: List of User UUIDs. Empty arrays are rejected with "Pick at least one assignee". + description + assignment_method + max_active_cases: Automatic-mode threshold: "fewer than N cases of this blueprint assigned". + escalation_enabled + escalation_blueprint: Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set. + transfer_enabled + four_eyes_checker_blueprint: Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue. + require_resolution_note + content_config: The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected. + is_active + default_deadline_hours: Default deadline for newly opened cases. Existing case deadlines are unchanged. + publish: Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseBlueprintDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/", + cast_to=CaseBlueprintDetail, + scope="application", + auth="api_key", + body={ + "name": name, + "assignee_pool": assignee_pool, + "description": description, + "assignment_method": assignment_method, + "max_active_cases": max_active_cases, + "escalation_enabled": escalation_enabled, + "escalation_blueprint": escalation_blueprint, + "transfer_enabled": transfer_enabled, + "four_eyes_checker_blueprint": four_eyes_checker_blueprint, + "require_resolution_note": require_resolution_note, + "content_config": content_config, + "is_active": is_active, + "default_deadline_hours": default_deadline_hours, + "publish": publish, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + blueprint_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseBlueprintDetail: + """Get blueprint. + + Retrieve a blueprint, including its connected creation sources (AML config, transaction rules, workflow nodes) and which blueprints escalate to it. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/`` + + Args: + blueprint_uuid: Blueprint UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseBlueprintDetail", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/", + cast_to=CaseBlueprintDetail, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + blueprint_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete blueprint. + + Delete a blueprint. Deletion always succeeds and does not touch the blueprint's cases: existing cases keep their blueprint reference and its settings continue to apply to them. A deleted blueprint no longer appears in blueprint lists and can no longer be chosen as a transfer target; if a maker blueprint is deleted while a resolution is pending 4-eyes approval, a rejected resolution leaves the case on the checker blueprint. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/`` + + Args: + blueprint_uuid: Blueprint UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/", + cast_to=None, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + blueprint_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | None | NotGiven = NOT_GIVEN, + assignee_pool: Sequence[str] | NotGiven = NOT_GIVEN, + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str | NotGiven = NOT_GIVEN, + max_active_cases: int | None | NotGiven = NOT_GIVEN, + escalation_enabled: bool | NotGiven = NOT_GIVEN, + escalation_blueprint: str | None | NotGiven = NOT_GIVEN, + transfer_enabled: bool | NotGiven = NOT_GIVEN, + four_eyes_checker_blueprint: str | None | NotGiven = NOT_GIVEN, + require_resolution_note: bool | NotGiven = NOT_GIVEN, + content_config: CaseContentConfigParam | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + default_deadline_hours: int | None | NotGiven = NOT_GIVEN, + expected_version: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseBlueprintDetail: + """Update blueprint. + + Partially update a blueprint's general settings or content_config. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/`` + + Args: + blueprint_uuid: Blueprint UUID. + name: Max 24 characters, e.g. "AML investigation". + description + assignee_pool: List of User UUIDs. Empty arrays are rejected with "Pick at least one assignee". + assignment_method + max_active_cases: Automatic-mode threshold: "fewer than N cases of this blueprint assigned". + escalation_enabled + escalation_blueprint: Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set. + transfer_enabled + four_eyes_checker_blueprint: Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue. + require_resolution_note + content_config: The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected. + is_active + default_deadline_hours: Default deadline for newly opened cases. Existing case deadlines are unchanged. + expected_version: The version opened by the editor. A stale version returns 409 without changing the review policy. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseBlueprintDetail", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/", + cast_to=CaseBlueprintDetail, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "description": description, + "assignee_pool": assignee_pool, + "assignment_method": assignment_method, + "max_active_cases": max_active_cases, + "escalation_enabled": escalation_enabled, + "escalation_blueprint": escalation_blueprint, + "transfer_enabled": transfer_enabled, + "four_eyes_checker_blueprint": four_eyes_checker_blueprint, + "require_resolution_note": require_resolution_note, + "content_config": content_config, + "is_active": is_active, + "default_deadline_hours": default_deadline_hours, + "expected_version": expected_version, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_presets( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBlueprintsListPresetsResponse: + """List blueprint presets. + + List the four built-in presets (Screening hit, AML investigation, Suspicious activity, KYC review) with their content_config and whether each is already installed on this application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBlueprintsListPresetsResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/", + cast_to=CasesBlueprintsListPresetsResponse, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def install_presets( + self, + *, + preset_keys: Sequence[str] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesBlueprintsInstallPresetsResponse: + """Install blueprint presets. + + Copy one or more presets into the application as editable blueprints. Idempotent per preset_key; omit preset_keys to install every preset not yet installed. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/`` + + Args: + preset_keys: Subset of screening_hit, aml_investigation, suspicious_activity, kyc. Omit to install all. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesBlueprintsInstallPresetsResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/", + cast_to=CasesBlueprintsInstallPresetsResponse, + scope="application", + auth="api_key", + body={"preset_keys": preset_keys}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def publish( + self, + blueprint_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasePublicationCaseBlueprint: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/`` + + Args: + blueprint_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasePublicationCaseBlueprint", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/", + cast_to=CasePublicationCaseBlueprint, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_draft( + self, + blueprint_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasePublicationPolicyDraftResponse: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/`` + + Args: + blueprint_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasePublicationPolicyDraftResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/", + cast_to=CasePublicationPolicyDraftResponse, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def save_draft( + self, + blueprint_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + changes: Mapping[str, object], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasePublicationPolicyDraft: + """``PUT /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/`` + + Args: + blueprint_uuid + expected_version + expected_revision + changes + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasePublicationPolicyDraft", + await self._client.request( + "put", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/", + cast_to=CasePublicationPolicyDraft, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={ + "expected_version": expected_version, + "expected_revision": expected_revision, + "changes": changes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def discard_draft( + self, + blueprint_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/`` + + Args: + blueprint_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/", + cast_to=None, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_history( + self, + blueprint_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CasePublicationPolicyRevision]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/`` + + Args: + blueprint_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CasePublicationPolicyRevision]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/", + cast_to=CasePublicationPaginatedPolicyRevisionList, + item_type=CasePublicationPolicyRevision, + path_params={"blueprint_uuid": blueprint_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesBlueprintsWithRawResponse: + def __init__(self, resource: CasesBlueprints) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + self.list_presets = to_raw_response_wrapper(resource.list_presets) + self.install_presets = to_raw_response_wrapper(resource.install_presets) + self.publish = to_raw_response_wrapper(resource.publish) + self.retrieve_draft = to_raw_response_wrapper(resource.retrieve_draft) + self.save_draft = to_raw_response_wrapper(resource.save_draft) + self.discard_draft = to_raw_response_wrapper(resource.discard_draft) + self.list_history = to_raw_response_wrapper(resource.list_history) + + +class AsyncCasesBlueprintsWithRawResponse: + def __init__(self, resource: AsyncCasesBlueprints) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + self.list_presets = async_to_raw_response_wrapper(resource.list_presets) + self.install_presets = async_to_raw_response_wrapper(resource.install_presets) + self.publish = async_to_raw_response_wrapper(resource.publish) + self.retrieve_draft = async_to_raw_response_wrapper(resource.retrieve_draft) + self.save_draft = async_to_raw_response_wrapper(resource.save_draft) + self.discard_draft = async_to_raw_response_wrapper(resource.discard_draft) + self.list_history = async_to_raw_response_wrapper(resource.list_history) + + +class CasesBlueprintsWithStreamingResponse: + def __init__(self, resource: CasesBlueprints) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + self.list_presets = to_streamed_response_wrapper(resource.list_presets) + self.install_presets = to_streamed_response_wrapper(resource.install_presets) + self.publish = to_streamed_response_wrapper(resource.publish) + self.retrieve_draft = to_streamed_response_wrapper(resource.retrieve_draft) + self.save_draft = to_streamed_response_wrapper(resource.save_draft) + self.discard_draft = to_streamed_response_wrapper(resource.discard_draft) + self.list_history = to_streamed_response_wrapper(resource.list_history) + + +class AsyncCasesBlueprintsWithStreamingResponse: + def __init__(self, resource: AsyncCasesBlueprints) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) + self.list_presets = async_to_streamed_response_wrapper(resource.list_presets) + self.install_presets = async_to_streamed_response_wrapper(resource.install_presets) + self.publish = async_to_streamed_response_wrapper(resource.publish) + self.retrieve_draft = async_to_streamed_response_wrapper(resource.retrieve_draft) + self.save_draft = async_to_streamed_response_wrapper(resource.save_draft) + self.discard_draft = async_to_streamed_response_wrapper(resource.discard_draft) + self.list_history = async_to_streamed_response_wrapper(resource.list_history) diff --git a/src/didit/resources/cases/checklist.py b/src/didit/resources/cases/checklist.py new file mode 100644 index 0000000..3ea5cbe --- /dev/null +++ b/src/didit/resources/cases/checklist.py @@ -0,0 +1,253 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.checklist`` resource.""" + +from __future__ import annotations + +import builtins +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.case_checklist_item import CaseChecklistItem + +__all__ = [ + "CasesChecklist", + "AsyncCasesChecklist", + "CasesChecklistWithRawResponse", + "AsyncCasesChecklistWithRawResponse", + "CasesChecklistWithStreamingResponse", + "AsyncCasesChecklistWithStreamingResponse", +] + + +class CasesChecklist(SyncAPIResource): + """The ``cases.checklist`` resource of the Didit API. + + Methods: ``list``, ``toggle_item``. + """ + + @cached_property + def with_raw_response(self) -> CasesChecklistWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesChecklistWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesChecklistWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesChecklistWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[CaseChecklistItem]: + """List case checklist. + + List the case's checklist items, copied from the blueprint's content_config.checklist.items at creation, ordered. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[CaseChecklistItem]", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/", + cast_to=builtins.list[CaseChecklistItem], + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def toggle_item( + self, + case_uuid: str, + item_uuid: str, + *, + is_completed: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseChecklistItem: + """Toggle checklist item. + + Mark a checklist item complete or incomplete. Resolution is blocked while any item is incomplete, unless the blueprint marks the checklist optional. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/`` + + Args: + case_uuid: Case UUID. + item_uuid: Checklist item UUID. + is_completed + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseChecklistItem", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/", + cast_to=CaseChecklistItem, + path_params={"case_uuid": case_uuid, "item_uuid": item_uuid}, + scope="application", + auth="api_key", + body={"is_completed": is_completed}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesChecklist(AsyncAPIResource): + """The ``cases.checklist`` resource of the Didit API. + + Methods: ``list``, ``toggle_item``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesChecklistWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesChecklistWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesChecklistWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesChecklistWithStreamingResponse(self) + + async def list( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[CaseChecklistItem]: + """List case checklist. + + List the case's checklist items, copied from the blueprint's content_config.checklist.items at creation, ordered. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[CaseChecklistItem]", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/", + cast_to=builtins.list[CaseChecklistItem], + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def toggle_item( + self, + case_uuid: str, + item_uuid: str, + *, + is_completed: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseChecklistItem: + """Toggle checklist item. + + Mark a checklist item complete or incomplete. Resolution is blocked while any item is incomplete, unless the blueprint marks the checklist optional. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/`` + + Args: + case_uuid: Case UUID. + item_uuid: Checklist item UUID. + is_completed + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseChecklistItem", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/", + cast_to=CaseChecklistItem, + path_params={"case_uuid": case_uuid, "item_uuid": item_uuid}, + scope="application", + auth="api_key", + body={"is_completed": is_completed}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesChecklistWithRawResponse: + def __init__(self, resource: CasesChecklist) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.toggle_item = to_raw_response_wrapper(resource.toggle_item) + + +class AsyncCasesChecklistWithRawResponse: + def __init__(self, resource: AsyncCasesChecklist) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.toggle_item = async_to_raw_response_wrapper(resource.toggle_item) + + +class CasesChecklistWithStreamingResponse: + def __init__(self, resource: CasesChecklist) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.toggle_item = to_streamed_response_wrapper(resource.toggle_item) + + +class AsyncCasesChecklistWithStreamingResponse: + def __init__(self, resource: AsyncCasesChecklist) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.toggle_item = async_to_streamed_response_wrapper(resource.toggle_item) diff --git a/src/didit/resources/cases/links.py b/src/didit/resources/cases/links.py new file mode 100644 index 0000000..17abec4 --- /dev/null +++ b/src/didit/resources/cases/links.py @@ -0,0 +1,337 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.links`` resource.""" + +from __future__ import annotations + +import builtins +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.case_link import CaseLink + +__all__ = [ + "CasesLinks", + "AsyncCasesLinks", + "CasesLinksWithRawResponse", + "AsyncCasesLinksWithRawResponse", + "CasesLinksWithStreamingResponse", + "AsyncCasesLinksWithStreamingResponse", +] + + +class CasesLinks(SyncAPIResource): + """The ``cases.links`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> CasesLinksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesLinksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesLinksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesLinksWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[CaseLink]: + """List case links. + + List sessions, business sessions, and transactions attached to the case. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[CaseLink]", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/", + cast_to=builtins.list[CaseLink], + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + case_uuid: str, + *, + entity_type: Literal["session", "business_session", "transaction"] | str, + entity_uuid: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseLink: + """Add case link. + + Attach a session, business session, or transaction to the case. Rejected with 400 if the entity belongs to a different subject than the case. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/`` + + Args: + case_uuid: Case UUID. + entity_type + entity_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseLink", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/", + cast_to=CaseLink, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"entity_type": entity_type, "entity_uuid": entity_uuid}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + case_uuid: str, + link_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Remove case link. + + Detach a linked session, business session, or transaction from the case. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/`` + + Args: + case_uuid: Case UUID. + link_uuid: Link UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/", + cast_to=None, + path_params={"case_uuid": case_uuid, "link_uuid": link_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesLinks(AsyncAPIResource): + """The ``cases.links`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesLinksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesLinksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesLinksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesLinksWithStreamingResponse(self) + + async def list( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[CaseLink]: + """List case links. + + List sessions, business sessions, and transactions attached to the case. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/`` + + Args: + case_uuid: Case UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[CaseLink]", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/", + cast_to=builtins.list[CaseLink], + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + case_uuid: str, + *, + entity_type: Literal["session", "business_session", "transaction"] | str, + entity_uuid: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseLink: + """Add case link. + + Attach a session, business session, or transaction to the case. Rejected with 400 if the entity belongs to a different subject than the case. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/`` + + Args: + case_uuid: Case UUID. + entity_type + entity_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseLink", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/", + cast_to=CaseLink, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"entity_type": entity_type, "entity_uuid": entity_uuid}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + case_uuid: str, + link_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Remove case link. + + Detach a linked session, business session, or transaction from the case. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/`` + + Args: + case_uuid: Case UUID. + link_uuid: Link UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/", + cast_to=None, + path_params={"case_uuid": case_uuid, "link_uuid": link_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesLinksWithRawResponse: + def __init__(self, resource: CasesLinks) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.delete = to_raw_response_wrapper(resource.delete) + + +class AsyncCasesLinksWithRawResponse: + def __init__(self, resource: AsyncCasesLinks) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.delete = async_to_raw_response_wrapper(resource.delete) + + +class CasesLinksWithStreamingResponse: + def __init__(self, resource: CasesLinks) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.delete = to_streamed_response_wrapper(resource.delete) + + +class AsyncCasesLinksWithStreamingResponse: + def __init__(self, resource: AsyncCasesLinks) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.delete = async_to_streamed_response_wrapper(resource.delete) diff --git a/src/didit/resources/cases/notes.py b/src/didit/resources/cases/notes.py new file mode 100644 index 0000000..931f39b --- /dev/null +++ b/src/didit/resources/cases/notes.py @@ -0,0 +1,652 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.notes`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.cases.notes.create_attachment_upload_response import ( + CasesNotesCreateAttachmentUploadResponse, +) +from didit.types.cases.notes.retrieve_attachment_url_response import ( + CasesNotesRetrieveAttachmentUrlResponse, +) +from didit.types.components.case_collaboration_case_note import CaseCollaborationCaseNote +from didit.types.components.case_collaboration_paginated_case_note_list import ( + CaseCollaborationPaginatedCaseNoteList, +) + +__all__ = [ + "CasesNotes", + "AsyncCasesNotes", + "CasesNotesWithRawResponse", + "AsyncCasesNotesWithRawResponse", + "CasesNotesWithStreamingResponse", + "AsyncCasesNotesWithStreamingResponse", +] + + +class CasesNotes(SyncAPIResource): + """The ``cases.notes`` resource of the Didit API. + + Methods: ``create``, ``create_attachment_upload``, ``delete``, ``list``, ``retrieve_attachment_url``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> CasesNotesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesNotesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesNotesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesNotesWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CaseCollaborationCaseNote]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/`` + + Args: + case_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CaseCollaborationCaseNote]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/", + cast_to=CaseCollaborationPaginatedCaseNoteList, + item_type=CaseCollaborationCaseNote, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + case_uuid: str, + *, + comment: str, + reply_to: str | None | NotGiven = NOT_GIVEN, + client_reference: str | None | NotGiven = NOT_GIVEN, + mentioned_emails: Sequence[str] | NotGiven = NOT_GIVEN, + is_internal: bool | NotGiven = NOT_GIVEN, + attachments: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseCollaborationCaseNote: + """Add a case-scoped note. An optional client_reference makes an unchanged retry return the same note without duplicate activity or notifications. reply_to must identify a live note in this case. The response includes a bounded parent excerpt without copying that excerpt into the saved reply. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/`` + + Args: + case_uuid + comment + reply_to + client_reference + mentioned_emails + is_internal + attachments + tags + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseCollaborationCaseNote", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/", + cast_to=CaseCollaborationCaseNote, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "comment": comment, + "reply_to": reply_to, + "client_reference": client_reference, + "mentioned_emails": mentioned_emails, + "is_internal": is_internal, + "attachments": attachments, + "tags": tags, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + case_uuid: str, + note_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/`` + + Args: + case_uuid + note_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/", + cast_to=None, + path_params={"case_uuid": case_uuid, "note_uuid": note_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + case_uuid: str, + note_uuid: str, + *, + is_pinned: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseCollaborationCaseNote: + """Pin or unpin an existing note. Pinned notes appear first. Pin changes are recorded in case activity and do not change evidence or source decisions. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/`` + + Args: + case_uuid + note_uuid + is_pinned + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseCollaborationCaseNote", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/", + cast_to=CaseCollaborationCaseNote, + path_params={"case_uuid": case_uuid, "note_uuid": note_uuid}, + scope="application", + auth="api_key", + body={"is_pinned": is_pinned}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_attachment_url( + self, + case_uuid: str, + *, + s3_key: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesNotesRetrieveAttachmentUrlResponse: + """Get note attachment URL. + + Mint a fresh presigned download URL for an attachment already stored on one of the case's notes. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/`` + + Args: + case_uuid: Case UUID. + s3_key: The attachment's s3_key, as returned by upload or listed on the note. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesNotesRetrieveAttachmentUrlResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/", + cast_to=CasesNotesRetrieveAttachmentUrlResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"s3_key": s3_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_attachment_upload( + self, + case_uuid: str, + *, + file: FileTypes, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesNotesCreateAttachmentUploadResponse: + """Upload note attachment. + + Upload a file (JPEG, PNG, WebP, PDF, plain text, or CSV; max 5 MB) to attach to a note. Returns the s3_key to pass in the note's attachments array. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/`` + + Args: + case_uuid: Case UUID. + file + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesNotesCreateAttachmentUploadResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/", + cast_to=CasesNotesCreateAttachmentUploadResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"file": file}, + multipart=True, + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesNotes(AsyncAPIResource): + """The ``cases.notes`` resource of the Didit API. + + Methods: ``create``, ``create_attachment_upload``, ``delete``, ``list``, ``retrieve_attachment_url``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesNotesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesNotesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesNotesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesNotesWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CaseCollaborationCaseNote]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/`` + + Args: + case_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CaseCollaborationCaseNote]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/", + cast_to=CaseCollaborationPaginatedCaseNoteList, + item_type=CaseCollaborationCaseNote, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + case_uuid: str, + *, + comment: str, + reply_to: str | None | NotGiven = NOT_GIVEN, + client_reference: str | None | NotGiven = NOT_GIVEN, + mentioned_emails: Sequence[str] | NotGiven = NOT_GIVEN, + is_internal: bool | NotGiven = NOT_GIVEN, + attachments: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseCollaborationCaseNote: + """Add a case-scoped note. An optional client_reference makes an unchanged retry return the same note without duplicate activity or notifications. reply_to must identify a live note in this case. The response includes a bounded parent excerpt without copying that excerpt into the saved reply. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/`` + + Args: + case_uuid + comment + reply_to + client_reference + mentioned_emails + is_internal + attachments + tags + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseCollaborationCaseNote", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/", + cast_to=CaseCollaborationCaseNote, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "comment": comment, + "reply_to": reply_to, + "client_reference": client_reference, + "mentioned_emails": mentioned_emails, + "is_internal": is_internal, + "attachments": attachments, + "tags": tags, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + case_uuid: str, + note_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/`` + + Args: + case_uuid + note_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/", + cast_to=None, + path_params={"case_uuid": case_uuid, "note_uuid": note_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + case_uuid: str, + note_uuid: str, + *, + is_pinned: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseCollaborationCaseNote: + """Pin or unpin an existing note. Pinned notes appear first. Pin changes are recorded in case activity and do not change evidence or source decisions. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/`` + + Args: + case_uuid + note_uuid + is_pinned + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseCollaborationCaseNote", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/", + cast_to=CaseCollaborationCaseNote, + path_params={"case_uuid": case_uuid, "note_uuid": note_uuid}, + scope="application", + auth="api_key", + body={"is_pinned": is_pinned}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_attachment_url( + self, + case_uuid: str, + *, + s3_key: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesNotesRetrieveAttachmentUrlResponse: + """Get note attachment URL. + + Mint a fresh presigned download URL for an attachment already stored on one of the case's notes. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/`` + + Args: + case_uuid: Case UUID. + s3_key: The attachment's s3_key, as returned by upload or listed on the note. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesNotesRetrieveAttachmentUrlResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/", + cast_to=CasesNotesRetrieveAttachmentUrlResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"s3_key": s3_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_attachment_upload( + self, + case_uuid: str, + *, + file: FileTypes, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CasesNotesCreateAttachmentUploadResponse: + """Upload note attachment. + + Upload a file (JPEG, PNG, WebP, PDF, plain text, or CSV; max 5 MB) to attach to a note. Returns the s3_key to pass in the note's attachments array. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/`` + + Args: + case_uuid: Case UUID. + file + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CasesNotesCreateAttachmentUploadResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/", + cast_to=CasesNotesCreateAttachmentUploadResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"file": file}, + multipart=True, + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesNotesWithRawResponse: + def __init__(self, resource: CasesNotes) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + self.retrieve_attachment_url = to_raw_response_wrapper(resource.retrieve_attachment_url) + self.create_attachment_upload = to_raw_response_wrapper(resource.create_attachment_upload) + + +class AsyncCasesNotesWithRawResponse: + def __init__(self, resource: AsyncCasesNotes) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + self.retrieve_attachment_url = async_to_raw_response_wrapper( + resource.retrieve_attachment_url + ) + self.create_attachment_upload = async_to_raw_response_wrapper( + resource.create_attachment_upload + ) + + +class CasesNotesWithStreamingResponse: + def __init__(self, resource: CasesNotes) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + self.retrieve_attachment_url = to_streamed_response_wrapper( + resource.retrieve_attachment_url + ) + self.create_attachment_upload = to_streamed_response_wrapper( + resource.create_attachment_upload + ) + + +class AsyncCasesNotesWithStreamingResponse: + def __init__(self, resource: AsyncCasesNotes) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) + self.retrieve_attachment_url = async_to_streamed_response_wrapper( + resource.retrieve_attachment_url + ) + self.create_attachment_upload = async_to_streamed_response_wrapper( + resource.create_attachment_upload + ) diff --git a/src/didit/resources/cases/regulatory_reports.py b/src/didit/resources/cases/regulatory_reports.py new file mode 100644 index 0000000..e23c2d2 --- /dev/null +++ b/src/didit/resources/cases/regulatory_reports.py @@ -0,0 +1,382 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.regulatory_reports`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.cases.regulatory_reports.create_params import ( + CasesRegulatoryReportsCreateParamsNarrative, +) +from didit.types.cases.regulatory_reports.list_response import CasesRegulatoryReportsListResponse +from didit.types.components.regulatory_report_detail import RegulatoryReportDetail + +__all__ = [ + "CasesRegulatoryReports", + "AsyncCasesRegulatoryReports", + "CasesRegulatoryReportsWithRawResponse", + "AsyncCasesRegulatoryReportsWithRawResponse", + "CasesRegulatoryReportsWithStreamingResponse", + "AsyncCasesRegulatoryReportsWithStreamingResponse", +] + + +class CasesRegulatoryReports(SyncAPIResource): + """The ``cases.regulatory_reports`` resource of the Didit API. + + Methods: ``create``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> CasesRegulatoryReportsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesRegulatoryReportsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesRegulatoryReportsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesRegulatoryReportsWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[RegulatoryReportDetail]: + """List reports for case. + + List FIU reports generated for the case. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/`` + + Args: + case_uuid: Case UUID. + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[RegulatoryReportDetail]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/", + cast_to=CasesRegulatoryReportsListResponse, + item_type=RegulatoryReportDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + case_uuid: str, + *, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + template: str | None | NotGiven = NOT_GIVEN, + transactions: Sequence[str] | None | NotGiven = NOT_GIVEN, + rentity_id: str | None | NotGiven = NOT_GIVEN, + rentity_branch: str | None | NotGiven = NOT_GIVEN, + submission_code: Literal["E", "M"] | str | NotGiven = NOT_GIVEN, + entity_reference: str | None | NotGiven = NOT_GIVEN, + fiu_ref_number: str | None | NotGiven = NOT_GIVEN, + currency_code_local: str | None | NotGiven = NOT_GIVEN, + submission_date: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + reporting_person: Mapping[str, object] | NotGiven = NOT_GIVEN, + location: Mapping[str, object] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, + action: str | None | NotGiven = NOT_GIVEN, + activity: str | None | NotGiven = NOT_GIVEN, + narrative: CasesRegulatoryReportsCreateParamsNarrative | NotGiven = NOT_GIVEN, + report_indicators: Sequence[str] | NotGiven = NOT_GIVEN, + included_fields: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Create report from case. + + Create a draft FIU report for the case: DRAFT status, auto-populated from the optional template plus the case's subject, linked transactions, and narrative. Validate then finalize to produce the goAML XML and PDF. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/`` + + Args: + case_uuid: Case UUID. + report_type: Required unless a template is given; the template's report_type is used otherwise. + jurisdiction: Required unless a template is given. + template: Report template UUID; its field_config seeds the draft before case-derived data is merged in. + transactions: Transactions to attach; defaults to the case's linked transactions. + rentity_id + rentity_branch + submission_code + entity_reference + fiu_ref_number + currency_code_local + submission_date + reporting_person + location + reason + action + activity + narrative + report_indicators + included_fields + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/", + cast_to=RegulatoryReportDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "report_type": report_type, + "jurisdiction": jurisdiction, + "template": template, + "transactions": transactions, + "rentity_id": rentity_id, + "rentity_branch": rentity_branch, + "submission_code": submission_code, + "entity_reference": entity_reference, + "fiu_ref_number": fiu_ref_number, + "currency_code_local": currency_code_local, + "submission_date": submission_date, + "reporting_person": reporting_person, + "location": location, + "reason": reason, + "action": action, + "activity": activity, + "narrative": narrative, + "report_indicators": report_indicators, + "included_fields": included_fields, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesRegulatoryReports(AsyncAPIResource): + """The ``cases.regulatory_reports`` resource of the Didit API. + + Methods: ``create``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesRegulatoryReportsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesRegulatoryReportsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesRegulatoryReportsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesRegulatoryReportsWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[RegulatoryReportDetail]: + """List reports for case. + + List FIU reports generated for the case. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/`` + + Args: + case_uuid: Case UUID. + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[RegulatoryReportDetail]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/", + cast_to=CasesRegulatoryReportsListResponse, + item_type=RegulatoryReportDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + case_uuid: str, + *, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + template: str | None | NotGiven = NOT_GIVEN, + transactions: Sequence[str] | None | NotGiven = NOT_GIVEN, + rentity_id: str | None | NotGiven = NOT_GIVEN, + rentity_branch: str | None | NotGiven = NOT_GIVEN, + submission_code: Literal["E", "M"] | str | NotGiven = NOT_GIVEN, + entity_reference: str | None | NotGiven = NOT_GIVEN, + fiu_ref_number: str | None | NotGiven = NOT_GIVEN, + currency_code_local: str | None | NotGiven = NOT_GIVEN, + submission_date: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + reporting_person: Mapping[str, object] | NotGiven = NOT_GIVEN, + location: Mapping[str, object] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, + action: str | None | NotGiven = NOT_GIVEN, + activity: str | None | NotGiven = NOT_GIVEN, + narrative: CasesRegulatoryReportsCreateParamsNarrative | NotGiven = NOT_GIVEN, + report_indicators: Sequence[str] | NotGiven = NOT_GIVEN, + included_fields: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Create report from case. + + Create a draft FIU report for the case: DRAFT status, auto-populated from the optional template plus the case's subject, linked transactions, and narrative. Validate then finalize to produce the goAML XML and PDF. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/`` + + Args: + case_uuid: Case UUID. + report_type: Required unless a template is given; the template's report_type is used otherwise. + jurisdiction: Required unless a template is given. + template: Report template UUID; its field_config seeds the draft before case-derived data is merged in. + transactions: Transactions to attach; defaults to the case's linked transactions. + rentity_id + rentity_branch + submission_code + entity_reference + fiu_ref_number + currency_code_local + submission_date + reporting_person + location + reason + action + activity + narrative + report_indicators + included_fields + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/", + cast_to=RegulatoryReportDetail, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "report_type": report_type, + "jurisdiction": jurisdiction, + "template": template, + "transactions": transactions, + "rentity_id": rentity_id, + "rentity_branch": rentity_branch, + "submission_code": submission_code, + "entity_reference": entity_reference, + "fiu_ref_number": fiu_ref_number, + "currency_code_local": currency_code_local, + "submission_date": submission_date, + "reporting_person": reporting_person, + "location": location, + "reason": reason, + "action": action, + "activity": activity, + "narrative": narrative, + "report_indicators": report_indicators, + "included_fields": included_fields, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesRegulatoryReportsWithRawResponse: + def __init__(self, resource: CasesRegulatoryReports) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + + +class AsyncCasesRegulatoryReportsWithRawResponse: + def __init__(self, resource: AsyncCasesRegulatoryReports) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + + +class CasesRegulatoryReportsWithStreamingResponse: + def __init__(self, resource: CasesRegulatoryReports) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + + +class AsyncCasesRegulatoryReportsWithStreamingResponse: + def __init__(self, resource: AsyncCasesRegulatoryReports) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) diff --git a/src/didit/resources/cases/requests.py b/src/didit/resources/cases/requests.py new file mode 100644 index 0000000..db5cfa0 --- /dev/null +++ b/src/didit/resources/cases/requests.py @@ -0,0 +1,615 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.requests`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.customer_messaging_case_request import CustomerMessagingCaseRequest +from didit.types.components.customer_messaging_case_request_options import ( + CustomerMessagingCaseRequestOptions, +) +from didit.types.components.customer_messaging_case_request_preview_response import ( + CustomerMessagingCaseRequestPreviewResponse, +) +from didit.types.components.customer_messaging_language_enum import CustomerMessagingLanguageEnum +from didit.types.components.customer_messaging_paginated_case_request_list import ( + CustomerMessagingPaginatedCaseRequestList, +) +from didit.types.components.customer_messaging_purpose_enum import CustomerMessagingPurposeEnum + +__all__ = [ + "CasesRequests", + "AsyncCasesRequests", + "CasesRequestsWithRawResponse", + "AsyncCasesRequestsWithRawResponse", + "CasesRequestsWithStreamingResponse", + "AsyncCasesRequestsWithStreamingResponse", +] + + +class CasesRequests(SyncAPIResource): + """The ``cases.requests`` resource of the Didit API. + + Methods: ``cancel``, ``create``, ``list``, ``preview``, ``retrieve_attachment``, ``retrieve_options``. + """ + + @cached_property + def with_raw_response(self) -> CasesRequestsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesRequestsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesRequestsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesRequestsWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CustomerMessagingCaseRequest]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/`` + + Args: + case_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CustomerMessagingCaseRequest]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/", + cast_to=CustomerMessagingPaginatedCaseRequestList, + item_type=CustomerMessagingCaseRequest, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + case_uuid: str, + *, + contact_id: str, + purpose: CustomerMessagingPurposeEnum, + request_id: str, + expires_at: str | _dt.datetime, + preview_token: str, + language: CustomerMessagingLanguageEnum | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequest: + """Confirm the exact signed preview within 15 minutes. Only fixed templates and an OTP-verified contact belonging to the case subject are accepted. The request ID makes retries idempotent. Delivery is queued; SENT, SIMULATED and UNKNOWN are distinct outcomes. An actionable request sets the case to Awaiting user. A review update leaves its status unchanged. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/`` + + Args: + case_uuid + contact_id + purpose: * `identity_document` - identity_document * `proof_of_address` - proof_of_address * `business_document` - business_document * `confirm_details` - confirm_details * `review_in_progress` - review_in_progress + request_id + expires_at + preview_token + language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequest", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/", + cast_to=CustomerMessagingCaseRequest, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "contact_id": contact_id, + "purpose": purpose, + "request_id": request_id, + "expires_at": expires_at, + "preview_token": preview_token, + "language": language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def cancel( + self, + case_uuid: str, + request_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequest: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/`` + + Args: + case_uuid + request_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequest", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/", + cast_to=CustomerMessagingCaseRequest, + path_params={"case_uuid": case_uuid, "request_uuid": request_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_options( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequestOptions: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/`` + + Args: + case_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequestOptions", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/", + cast_to=CustomerMessagingCaseRequestOptions, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_attachment( + self, + case_uuid: str, + request_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/`` + + Args: + case_uuid + request_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/", + cast_to=bytes, + path_params={"case_uuid": case_uuid, "request_uuid": request_uuid}, + scope="application", + auth="api_key", + accept="image/jpeg, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def preview( + self, + case_uuid: str, + *, + contact_id: str, + purpose: CustomerMessagingPurposeEnum, + language: CustomerMessagingLanguageEnum | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequestPreviewResponse: + """Render the production email without sending or changing the case. Returns the exact recipient, sender, subject, body and a signed preview_token. Send the same contact_id, purpose, language, request_id, expires_at and preview_token to confirm within 15 minutes. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/`` + + Args: + case_uuid + contact_id + purpose: * `identity_document` - identity_document * `proof_of_address` - proof_of_address * `business_document` - business_document * `confirm_details` - confirm_details * `review_in_progress` - review_in_progress + language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequestPreviewResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/", + cast_to=CustomerMessagingCaseRequestPreviewResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"contact_id": contact_id, "purpose": purpose, "language": language}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesRequests(AsyncAPIResource): + """The ``cases.requests`` resource of the Didit API. + + Methods: ``cancel``, ``create``, ``list``, ``preview``, ``retrieve_attachment``, ``retrieve_options``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesRequestsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesRequestsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesRequestsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesRequestsWithStreamingResponse(self) + + def list( + self, + case_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CustomerMessagingCaseRequest]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/`` + + Args: + case_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CustomerMessagingCaseRequest]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/", + cast_to=CustomerMessagingPaginatedCaseRequestList, + item_type=CustomerMessagingCaseRequest, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + case_uuid: str, + *, + contact_id: str, + purpose: CustomerMessagingPurposeEnum, + request_id: str, + expires_at: str | _dt.datetime, + preview_token: str, + language: CustomerMessagingLanguageEnum | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequest: + """Confirm the exact signed preview within 15 minutes. Only fixed templates and an OTP-verified contact belonging to the case subject are accepted. The request ID makes retries idempotent. Delivery is queued; SENT, SIMULATED and UNKNOWN are distinct outcomes. An actionable request sets the case to Awaiting user. A review update leaves its status unchanged. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/`` + + Args: + case_uuid + contact_id + purpose: * `identity_document` - identity_document * `proof_of_address` - proof_of_address * `business_document` - business_document * `confirm_details` - confirm_details * `review_in_progress` - review_in_progress + request_id + expires_at + preview_token + language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequest", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/", + cast_to=CustomerMessagingCaseRequest, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={ + "contact_id": contact_id, + "purpose": purpose, + "request_id": request_id, + "expires_at": expires_at, + "preview_token": preview_token, + "language": language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def cancel( + self, + case_uuid: str, + request_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequest: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/`` + + Args: + case_uuid + request_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequest", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/", + cast_to=CustomerMessagingCaseRequest, + path_params={"case_uuid": case_uuid, "request_uuid": request_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_options( + self, + case_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequestOptions: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/`` + + Args: + case_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequestOptions", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/", + cast_to=CustomerMessagingCaseRequestOptions, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_attachment( + self, + case_uuid: str, + request_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/`` + + Args: + case_uuid + request_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/", + cast_to=bytes, + path_params={"case_uuid": case_uuid, "request_uuid": request_uuid}, + scope="application", + auth="api_key", + accept="image/jpeg, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def preview( + self, + case_uuid: str, + *, + contact_id: str, + purpose: CustomerMessagingPurposeEnum, + language: CustomerMessagingLanguageEnum | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CustomerMessagingCaseRequestPreviewResponse: + """Render the production email without sending or changing the case. Returns the exact recipient, sender, subject, body and a signed preview_token. Send the same contact_id, purpose, language, request_id, expires_at and preview_token to confirm within 15 minutes. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/`` + + Args: + case_uuid + contact_id + purpose: * `identity_document` - identity_document * `proof_of_address` - proof_of_address * `business_document` - business_document * `confirm_details` - confirm_details * `review_in_progress` - review_in_progress + language + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CustomerMessagingCaseRequestPreviewResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/", + cast_to=CustomerMessagingCaseRequestPreviewResponse, + path_params={"case_uuid": case_uuid}, + scope="application", + auth="api_key", + body={"contact_id": contact_id, "purpose": purpose, "language": language}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesRequestsWithRawResponse: + def __init__(self, resource: CasesRequests) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.cancel = to_raw_response_wrapper(resource.cancel) + self.retrieve_options = to_raw_response_wrapper(resource.retrieve_options) + self.retrieve_attachment = to_raw_response_wrapper(resource.retrieve_attachment) + self.preview = to_raw_response_wrapper(resource.preview) + + +class AsyncCasesRequestsWithRawResponse: + def __init__(self, resource: AsyncCasesRequests) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.cancel = async_to_raw_response_wrapper(resource.cancel) + self.retrieve_options = async_to_raw_response_wrapper(resource.retrieve_options) + self.retrieve_attachment = async_to_raw_response_wrapper(resource.retrieve_attachment) + self.preview = async_to_raw_response_wrapper(resource.preview) + + +class CasesRequestsWithStreamingResponse: + def __init__(self, resource: CasesRequests) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.cancel = to_streamed_response_wrapper(resource.cancel) + self.retrieve_options = to_streamed_response_wrapper(resource.retrieve_options) + self.retrieve_attachment = to_streamed_response_wrapper(resource.retrieve_attachment) + self.preview = to_streamed_response_wrapper(resource.preview) + + +class AsyncCasesRequestsWithStreamingResponse: + def __init__(self, resource: AsyncCasesRequests) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.cancel = async_to_streamed_response_wrapper(resource.cancel) + self.retrieve_options = async_to_streamed_response_wrapper(resource.retrieve_options) + self.retrieve_attachment = async_to_streamed_response_wrapper(resource.retrieve_attachment) + self.preview = async_to_streamed_response_wrapper(resource.preview) diff --git a/src/didit/resources/cases/routing_rules.py b/src/didit/resources/cases/routing_rules.py new file mode 100644 index 0000000..39590f0 --- /dev/null +++ b/src/didit/resources/cases/routing_rules.py @@ -0,0 +1,1062 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.routing_rules`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.cases.routing_rules.list_response import CasesRoutingRulesListResponse +from didit.types.components.case_routing_condition import CaseRoutingConditionParam +from didit.types.components.case_routing_preview import CaseRoutingPreview +from didit.types.components.case_routing_rule import CaseRoutingRule +from didit.types.components.case_routing_rule_create import CaseRoutingRuleCreate +from didit.types.components.routing_publication_case_routing_rule import ( + RoutingPublicationCaseRoutingRule, +) +from didit.types.components.routing_publication_paginated_policy_revision_list import ( + RoutingPublicationPaginatedPolicyRevisionList, +) +from didit.types.components.routing_publication_policy_draft import RoutingPublicationPolicyDraft +from didit.types.components.routing_publication_policy_draft_response import ( + RoutingPublicationPolicyDraftResponse, +) +from didit.types.components.routing_publication_policy_revision import ( + RoutingPublicationPolicyRevision, +) + +__all__ = [ + "CasesRoutingRules", + "AsyncCasesRoutingRules", + "CasesRoutingRulesWithRawResponse", + "AsyncCasesRoutingRulesWithRawResponse", + "CasesRoutingRulesWithStreamingResponse", + "AsyncCasesRoutingRulesWithStreamingResponse", +] + + +class CasesRoutingRules(SyncAPIResource): + """The ``cases.routing_rules`` resource of the Didit API. + + Methods: ``create``, ``discard_draft``, ``list``, ``list_history``, ``preview``, ``publish``, ``retrieve``, ``retrieve_draft``, ``save_draft``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> CasesRoutingRulesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesRoutingRulesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesRoutingRulesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesRoutingRulesWithStreamingResponse(self) + + def list_history( + self, + record_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[RoutingPublicationPolicyRevision]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/`` + + Args: + record_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[RoutingPublicationPolicyRevision]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/", + cast_to=RoutingPublicationPaginatedPolicyRevisionList, + item_type=RoutingPublicationPolicyRevision, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + record_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingRule: + """Retrieve routing rule. + + Requires `read:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/`` + + Args: + record_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingRule", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/", + cast_to=CaseRoutingRule, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + record_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + priority: int | NotGiven = NOT_GIVEN, + conditions: Sequence[CaseRoutingConditionParam] | NotGiven = NOT_GIVEN, + required_skills: Sequence[str] | NotGiven = NOT_GIVEN, + blueprint: str | NotGiven = NOT_GIVEN, + team: str | None | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + expected_version: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingRule: + """Update routing rule. + + Requires `write:cases` for the requested application. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/`` + + Args: + record_uuid + name + description + priority + conditions + required_skills + blueprint + team + is_active + expected_version: Version opened by the editor. When supplied, stale writes return 409 without changing the rule. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingRule", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/", + cast_to=CaseRoutingRule, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "description": description, + "priority": priority, + "conditions": conditions, + "required_skills": required_skills, + "blueprint": blueprint, + "team": team, + "is_active": is_active, + "expected_version": expected_version, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CaseRoutingRule]: + """List routing rules. + + Requires `list:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/`` + + Args: + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CaseRoutingRule]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/", + cast_to=CasesRoutingRulesListResponse, + item_type=CaseRoutingRule, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + name: str, + conditions: Sequence[CaseRoutingConditionParam], + blueprint: str, + description: str | NotGiven = NOT_GIVEN, + priority: int | NotGiven = NOT_GIVEN, + required_skills: Sequence[str] | NotGiven = NOT_GIVEN, + team: str | None | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + publish: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingRule: + """Create routing rule. + + Requires `write:cases` for the requested application. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/`` + + Args: + name + conditions + blueprint + description + priority + required_skills + team + is_active + publish: Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingRule", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/", + cast_to=CaseRoutingRule, + scope="application", + auth="api_key", + body={ + "name": name, + "conditions": conditions, + "blueprint": blueprint, + "description": description, + "priority": priority, + "required_skills": required_skills, + "team": team, + "is_active": is_active, + "publish": publish, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def publish( + self, + record_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RoutingPublicationCaseRoutingRule: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/`` + + Args: + record_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RoutingPublicationCaseRoutingRule", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/", + cast_to=RoutingPublicationCaseRoutingRule, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_draft( + self, + record_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RoutingPublicationPolicyDraftResponse: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/`` + + Args: + record_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RoutingPublicationPolicyDraftResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/", + cast_to=RoutingPublicationPolicyDraftResponse, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def save_draft( + self, + record_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + changes: Mapping[str, object], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RoutingPublicationPolicyDraft: + """``PUT /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/`` + + Args: + record_uuid + expected_version + expected_revision + changes + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RoutingPublicationPolicyDraft", + self._client.request( + "put", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/", + cast_to=RoutingPublicationPolicyDraft, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={ + "expected_version": expected_version, + "expected_revision": expected_revision, + "changes": changes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def discard_draft( + self, + record_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/`` + + Args: + record_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/", + cast_to=None, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def preview( + self, + *, + source: Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] | str, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + rule_uuid: str | NotGiven = NOT_GIVEN, + draft: CaseRoutingRuleCreate | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingPreview: + """Test case routing. + + Requires `read:cases` for the requested application. Evaluates current rules or a supplied draft against current reviewer access, availability, skills and capacity. It does not save the draft or reserve reviewer capacity. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/`` + + Args: + source + priority + tags + rule_uuid + draft + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingPreview", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/", + cast_to=CaseRoutingPreview, + scope="application", + auth="api_key", + body={ + "source": source, + "priority": priority, + "tags": tags, + "rule_uuid": rule_uuid, + "draft": draft, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesRoutingRules(AsyncAPIResource): + """The ``cases.routing_rules`` resource of the Didit API. + + Methods: ``create``, ``discard_draft``, ``list``, ``list_history``, ``preview``, ``publish``, ``retrieve``, ``retrieve_draft``, ``save_draft``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesRoutingRulesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesRoutingRulesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesRoutingRulesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesRoutingRulesWithStreamingResponse(self) + + def list_history( + self, + record_uuid: str, + *, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[RoutingPublicationPolicyRevision]: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/`` + + Args: + record_uuid + ordering: Which field to use when ordering the results. + limit: Number of results to return per page. + offset: The initial index from which to return the results. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[RoutingPublicationPolicyRevision]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/", + cast_to=RoutingPublicationPaginatedPolicyRevisionList, + item_type=RoutingPublicationPolicyRevision, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + query={"ordering": ordering, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + record_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingRule: + """Retrieve routing rule. + + Requires `read:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/`` + + Args: + record_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingRule", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/", + cast_to=CaseRoutingRule, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + record_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + priority: int | NotGiven = NOT_GIVEN, + conditions: Sequence[CaseRoutingConditionParam] | NotGiven = NOT_GIVEN, + required_skills: Sequence[str] | NotGiven = NOT_GIVEN, + blueprint: str | NotGiven = NOT_GIVEN, + team: str | None | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + expected_version: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingRule: + """Update routing rule. + + Requires `write:cases` for the requested application. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/`` + + Args: + record_uuid + name + description + priority + conditions + required_skills + blueprint + team + is_active + expected_version: Version opened by the editor. When supplied, stale writes return 409 without changing the rule. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingRule", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/", + cast_to=CaseRoutingRule, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "description": description, + "priority": priority, + "conditions": conditions, + "required_skills": required_skills, + "blueprint": blueprint, + "team": team, + "is_active": is_active, + "expected_version": expected_version, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CaseRoutingRule]: + """List routing rules. + + Requires `list:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/`` + + Args: + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CaseRoutingRule]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/", + cast_to=CasesRoutingRulesListResponse, + item_type=CaseRoutingRule, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + name: str, + conditions: Sequence[CaseRoutingConditionParam], + blueprint: str, + description: str | NotGiven = NOT_GIVEN, + priority: int | NotGiven = NOT_GIVEN, + required_skills: Sequence[str] | NotGiven = NOT_GIVEN, + team: str | None | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + publish: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingRule: + """Create routing rule. + + Requires `write:cases` for the requested application. Set publish=false to create an unpublished draft; Publish creates its first live version. Discarding a never-published draft deletes it. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/`` + + Args: + name + conditions + blueprint + description + priority + required_skills + team + is_active + publish: Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingRule", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/", + cast_to=CaseRoutingRule, + scope="application", + auth="api_key", + body={ + "name": name, + "conditions": conditions, + "blueprint": blueprint, + "description": description, + "priority": priority, + "required_skills": required_skills, + "team": team, + "is_active": is_active, + "publish": publish, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def publish( + self, + record_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RoutingPublicationCaseRoutingRule: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/`` + + Args: + record_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RoutingPublicationCaseRoutingRule", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/", + cast_to=RoutingPublicationCaseRoutingRule, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_draft( + self, + record_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RoutingPublicationPolicyDraftResponse: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/`` + + Args: + record_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RoutingPublicationPolicyDraftResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/", + cast_to=RoutingPublicationPolicyDraftResponse, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def save_draft( + self, + record_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + changes: Mapping[str, object], + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RoutingPublicationPolicyDraft: + """``PUT /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/`` + + Args: + record_uuid + expected_version + expected_revision + changes + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RoutingPublicationPolicyDraft", + await self._client.request( + "put", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/", + cast_to=RoutingPublicationPolicyDraft, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={ + "expected_version": expected_version, + "expected_revision": expected_revision, + "changes": changes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def discard_draft( + self, + record_uuid: str, + *, + expected_version: int, + expected_revision: str | None, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/`` + + Args: + record_uuid + expected_version + expected_revision + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/", + cast_to=None, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={"expected_version": expected_version, "expected_revision": expected_revision}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def preview( + self, + *, + source: Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] | str, + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | NotGiven = NOT_GIVEN, + tags: Sequence[str] | NotGiven = NOT_GIVEN, + rule_uuid: str | NotGiven = NOT_GIVEN, + draft: CaseRoutingRuleCreate | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseRoutingPreview: + """Test case routing. + + Requires `read:cases` for the requested application. Evaluates current rules or a supplied draft against current reviewer access, availability, skills and capacity. It does not save the draft or reserve reviewer capacity. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/`` + + Args: + source + priority + tags + rule_uuid + draft + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseRoutingPreview", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/", + cast_to=CaseRoutingPreview, + scope="application", + auth="api_key", + body={ + "source": source, + "priority": priority, + "tags": tags, + "rule_uuid": rule_uuid, + "draft": draft, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesRoutingRulesWithRawResponse: + def __init__(self, resource: CasesRoutingRules) -> None: + self._resource = resource + self.list_history = to_raw_response_wrapper(resource.list_history) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.update = to_raw_response_wrapper(resource.update) + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.publish = to_raw_response_wrapper(resource.publish) + self.retrieve_draft = to_raw_response_wrapper(resource.retrieve_draft) + self.save_draft = to_raw_response_wrapper(resource.save_draft) + self.discard_draft = to_raw_response_wrapper(resource.discard_draft) + self.preview = to_raw_response_wrapper(resource.preview) + + +class AsyncCasesRoutingRulesWithRawResponse: + def __init__(self, resource: AsyncCasesRoutingRules) -> None: + self._resource = resource + self.list_history = async_to_raw_response_wrapper(resource.list_history) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.update = async_to_raw_response_wrapper(resource.update) + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.publish = async_to_raw_response_wrapper(resource.publish) + self.retrieve_draft = async_to_raw_response_wrapper(resource.retrieve_draft) + self.save_draft = async_to_raw_response_wrapper(resource.save_draft) + self.discard_draft = async_to_raw_response_wrapper(resource.discard_draft) + self.preview = async_to_raw_response_wrapper(resource.preview) + + +class CasesRoutingRulesWithStreamingResponse: + def __init__(self, resource: CasesRoutingRules) -> None: + self._resource = resource + self.list_history = to_streamed_response_wrapper(resource.list_history) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.update = to_streamed_response_wrapper(resource.update) + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.publish = to_streamed_response_wrapper(resource.publish) + self.retrieve_draft = to_streamed_response_wrapper(resource.retrieve_draft) + self.save_draft = to_streamed_response_wrapper(resource.save_draft) + self.discard_draft = to_streamed_response_wrapper(resource.discard_draft) + self.preview = to_streamed_response_wrapper(resource.preview) + + +class AsyncCasesRoutingRulesWithStreamingResponse: + def __init__(self, resource: AsyncCasesRoutingRules) -> None: + self._resource = resource + self.list_history = async_to_streamed_response_wrapper(resource.list_history) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.update = async_to_streamed_response_wrapper(resource.update) + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.publish = async_to_streamed_response_wrapper(resource.publish) + self.retrieve_draft = async_to_streamed_response_wrapper(resource.retrieve_draft) + self.save_draft = async_to_streamed_response_wrapper(resource.save_draft) + self.discard_draft = async_to_streamed_response_wrapper(resource.discard_draft) + self.preview = async_to_streamed_response_wrapper(resource.preview) diff --git a/src/didit/resources/cases/teams.py b/src/didit/resources/cases/teams.py new file mode 100644 index 0000000..02660f9 --- /dev/null +++ b/src/didit/resources/cases/teams.py @@ -0,0 +1,461 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``cases.teams`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.cases.teams.list_response import CasesTeamsListResponse +from didit.types.components.case_review_team import CaseReviewTeam +from didit.types.components.case_review_team_member import CaseReviewTeamMemberParam + +__all__ = [ + "CasesTeams", + "AsyncCasesTeams", + "CasesTeamsWithRawResponse", + "AsyncCasesTeamsWithRawResponse", + "CasesTeamsWithStreamingResponse", + "AsyncCasesTeamsWithStreamingResponse", +] + + +class CasesTeams(SyncAPIResource): + """The ``cases.teams`` resource of the Didit API. + + Methods: ``create``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> CasesTeamsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CasesTeamsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CasesTeamsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CasesTeamsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[CaseReviewTeam]: + """List review teams. + + Requires `list:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/`` + + Args: + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[CaseReviewTeam]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/", + cast_to=CasesTeamsListResponse, + item_type=CaseReviewTeam, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + name: str, + members: Sequence[CaseReviewTeamMemberParam], + description: str | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseReviewTeam: + """Create review team. + + Requires `write:cases` for the requested application. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/teams/`` + + Args: + name + members + description + is_active + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseReviewTeam", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/", + cast_to=CaseReviewTeam, + scope="application", + auth="api_key", + body={ + "name": name, + "members": members, + "description": description, + "is_active": is_active, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + record_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseReviewTeam: + """Retrieve review team. + + Requires `read:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/`` + + Args: + record_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseReviewTeam", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/", + cast_to=CaseReviewTeam, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + record_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + members: Sequence[CaseReviewTeamMemberParam] | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseReviewTeam: + """Update review team. + + Requires `write:cases` for the requested application. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/`` + + Args: + record_uuid + name + description + members + is_active + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseReviewTeam", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/", + cast_to=CaseReviewTeam, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "description": description, + "members": members, + "is_active": is_active, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCasesTeams(AsyncAPIResource): + """The ``cases.teams`` resource of the Didit API. + + Methods: ``create``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCasesTeamsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCasesTeamsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCasesTeamsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCasesTeamsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[CaseReviewTeam]: + """List review teams. + + Requires `list:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/`` + + Args: + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[CaseReviewTeam]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/", + cast_to=CasesTeamsListResponse, + item_type=CaseReviewTeam, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + name: str, + members: Sequence[CaseReviewTeamMemberParam], + description: str | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseReviewTeam: + """Create review team. + + Requires `write:cases` for the requested application. + + ``POST /v3/organization/{organization_id}/application/{application_id}/cases/teams/`` + + Args: + name + members + description + is_active + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseReviewTeam", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/", + cast_to=CaseReviewTeam, + scope="application", + auth="api_key", + body={ + "name": name, + "members": members, + "description": description, + "is_active": is_active, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + record_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseReviewTeam: + """Retrieve review team. + + Requires `read:cases` for the requested application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/`` + + Args: + record_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseReviewTeam", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/", + cast_to=CaseReviewTeam, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + record_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + description: str | NotGiven = NOT_GIVEN, + members: Sequence[CaseReviewTeamMemberParam] | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> CaseReviewTeam: + """Update review team. + + Requires `write:cases` for the requested application. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/`` + + Args: + record_uuid + name + description + members + is_active + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "CaseReviewTeam", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/", + cast_to=CaseReviewTeam, + path_params={"record_uuid": record_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "description": description, + "members": members, + "is_active": is_active, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CasesTeamsWithRawResponse: + def __init__(self, resource: CasesTeams) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncCasesTeamsWithRawResponse: + def __init__(self, resource: AsyncCasesTeams) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.update = async_to_raw_response_wrapper(resource.update) + + +class CasesTeamsWithStreamingResponse: + def __init__(self, resource: CasesTeams) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncCasesTeamsWithStreamingResponse: + def __init__(self, resource: AsyncCasesTeams) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/customization.py b/src/didit/resources/customization.py new file mode 100644 index 0000000..d9eab2a --- /dev/null +++ b/src/didit/resources/customization.py @@ -0,0 +1,416 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``customization`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.components.branding_customization import BrandingCustomization + +__all__ = [ + "Customization", + "AsyncCustomization", + "CustomizationWithRawResponse", + "AsyncCustomizationWithRawResponse", + "CustomizationWithStreamingResponse", + "AsyncCustomizationWithStreamingResponse", +] + + +class Customization(SyncAPIResource): + """The ``customization`` resource of the Didit API. + + Methods: ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> CustomizationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return CustomizationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> CustomizationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return CustomizationWithStreamingResponse(self) + + def retrieve( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrandingCustomization: + """Get branding customization. + + Returns the white-label branding for the application resolved from the API key: colors, fonts, border radii, logo URLs and UI/UX options. Custom domain and white-label email settings are managed only in the Business Console and are not part of this response. + + ``GET /v3/customization/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BrandingCustomization", + self._client.request( + "get", + "/v3/customization/", + cast_to=BrandingCustomization, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + *, + color_primary: str | NotGiven = NOT_GIVEN, + color_secondary: str | NotGiven = NOT_GIVEN, + color_background: str | NotGiven = NOT_GIVEN, + color_panel: str | NotGiven = NOT_GIVEN, + color_panel_10: str | NotGiven = NOT_GIVEN, + color_on_panel_1: str | NotGiven = NOT_GIVEN, + color_on_panel_2: str | NotGiven = NOT_GIVEN, + color_on_background: str | NotGiven = NOT_GIVEN, + color_button_1: str | NotGiven = NOT_GIVEN, + color_button_2: str | NotGiven = NOT_GIVEN, + color_button_text_1: str | NotGiven = NOT_GIVEN, + color_button_text_2: str | NotGiven = NOT_GIVEN, + color_pill_text: str | NotGiven = NOT_GIVEN, + border_radius_panel: int | NotGiven = NOT_GIVEN, + border_radius_buttons: int | NotGiven = NOT_GIVEN, + font_family: str | NotGiven = NOT_GIVEN, + font_weight: str | NotGiven = NOT_GIVEN, + app_public_name: str | None | NotGiven = NOT_GIVEN, + privacy_policy_url: str | None | NotGiven = NOT_GIVEN, + skip_welcome_screen: bool | NotGiven = NOT_GIVEN, + disable_login_with_didit: bool | NotGiven = NOT_GIVEN, + hide_progress_bar: bool | NotGiven = NOT_GIVEN, + callback_seconds: int | NotGiven = NOT_GIVEN, + delete_logo_square: bool | NotGiven = NOT_GIVEN, + delete_logo_rectangular: bool | NotGiven = NOT_GIVEN, + delete_favicon: bool | NotGiven = NOT_GIVEN, + image_square: FileTypes | NotGiven = NOT_GIVEN, + image_rectangular: FileTypes | NotGiven = NOT_GIVEN, + image_favicon: FileTypes | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrandingCustomization: + """Update branding customization. + + Partial update of the application's white-label branding. Send `application/json` to change colors, fonts, radii and UI options, or `multipart/form-data` to upload logo images. Only the fields you include are changed. Custom domain and white-label email cannot be set through this endpoint - use the Business Console for those. + + ``PATCH /v3/customization/`` + + Args: + color_primary: Headers / primary text color. + color_secondary: Paragraph / secondary text color. + color_background: Page background color. + color_panel: Panel / accent color. + color_panel_10: Panel color at 10% opacity. + color_on_panel_1: Support text color on panels. + color_on_panel_2: Pill / badge color on panels. + color_on_background: Text color on background. + color_button_1: Primary button color. + color_button_2: Secondary button color. + color_button_text_1: Primary button text color. + color_button_text_2: Secondary button text color. + color_pill_text: Text color for pills, inputs and selectors. + border_radius_panel: Panel corner radius in pixels. + border_radius_buttons: Button corner radius in pixels. + font_family: Google Font family name (e.g. `Inter`, `AR One Sans`). + font_weight: Font weight available for the selected family (e.g. `regular`, `500`, `700`). + app_public_name: Public name shown in the verification flow. + privacy_policy_url: Privacy policy URL shown to the end user. + skip_welcome_screen: Skip the initial welcome screen. + disable_login_with_didit: Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400). + hide_progress_bar: Hide the progress bar during the flow. + callback_seconds: Delay (seconds) before redirecting to the callback URL. + delete_logo_square: When `true`, removes the current square logo. + delete_logo_rectangular: When `true`, removes the current rectangular logo. + delete_favicon: When `true`, removes the current favicon. + image_square: Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. + image_rectangular: Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. + image_favicon: Favicon image. Max **500 KB**. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BrandingCustomization", + self._client.request( + "patch", + "/v3/customization/", + cast_to=BrandingCustomization, + auth="api_key", + body={ + "color_primary": color_primary, + "color_secondary": color_secondary, + "color_background": color_background, + "color_panel": color_panel, + "color_panel_10": color_panel_10, + "color_on_panel_1": color_on_panel_1, + "color_on_panel_2": color_on_panel_2, + "color_on_background": color_on_background, + "color_button_1": color_button_1, + "color_button_2": color_button_2, + "color_button_text_1": color_button_text_1, + "color_button_text_2": color_button_text_2, + "color_pill_text": color_pill_text, + "border_radius_panel": border_radius_panel, + "border_radius_buttons": border_radius_buttons, + "font_family": font_family, + "font_weight": font_weight, + "app_public_name": app_public_name, + "privacy_policy_url": privacy_policy_url, + "skip_welcome_screen": skip_welcome_screen, + "disable_login_with_didit": disable_login_with_didit, + "hide_progress_bar": hide_progress_bar, + "callback_seconds": callback_seconds, + "delete_logo_square": delete_logo_square, + "delete_logo_rectangular": delete_logo_rectangular, + "delete_favicon": delete_favicon, + "image_square": image_square, + "image_rectangular": image_rectangular, + "image_favicon": image_favicon, + }, + multipart="auto", + file_fields=( + "image_favicon", + "image_rectangular", + "image_square", + ), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncCustomization(AsyncAPIResource): + """The ``customization`` resource of the Didit API. + + Methods: ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncCustomizationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncCustomizationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncCustomizationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncCustomizationWithStreamingResponse(self) + + async def retrieve( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrandingCustomization: + """Get branding customization. + + Returns the white-label branding for the application resolved from the API key: colors, fonts, border radii, logo URLs and UI/UX options. Custom domain and white-label email settings are managed only in the Business Console and are not part of this response. + + ``GET /v3/customization/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BrandingCustomization", + await self._client.request( + "get", + "/v3/customization/", + cast_to=BrandingCustomization, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + *, + color_primary: str | NotGiven = NOT_GIVEN, + color_secondary: str | NotGiven = NOT_GIVEN, + color_background: str | NotGiven = NOT_GIVEN, + color_panel: str | NotGiven = NOT_GIVEN, + color_panel_10: str | NotGiven = NOT_GIVEN, + color_on_panel_1: str | NotGiven = NOT_GIVEN, + color_on_panel_2: str | NotGiven = NOT_GIVEN, + color_on_background: str | NotGiven = NOT_GIVEN, + color_button_1: str | NotGiven = NOT_GIVEN, + color_button_2: str | NotGiven = NOT_GIVEN, + color_button_text_1: str | NotGiven = NOT_GIVEN, + color_button_text_2: str | NotGiven = NOT_GIVEN, + color_pill_text: str | NotGiven = NOT_GIVEN, + border_radius_panel: int | NotGiven = NOT_GIVEN, + border_radius_buttons: int | NotGiven = NOT_GIVEN, + font_family: str | NotGiven = NOT_GIVEN, + font_weight: str | NotGiven = NOT_GIVEN, + app_public_name: str | None | NotGiven = NOT_GIVEN, + privacy_policy_url: str | None | NotGiven = NOT_GIVEN, + skip_welcome_screen: bool | NotGiven = NOT_GIVEN, + disable_login_with_didit: bool | NotGiven = NOT_GIVEN, + hide_progress_bar: bool | NotGiven = NOT_GIVEN, + callback_seconds: int | NotGiven = NOT_GIVEN, + delete_logo_square: bool | NotGiven = NOT_GIVEN, + delete_logo_rectangular: bool | NotGiven = NOT_GIVEN, + delete_favicon: bool | NotGiven = NOT_GIVEN, + image_square: FileTypes | NotGiven = NOT_GIVEN, + image_rectangular: FileTypes | NotGiven = NOT_GIVEN, + image_favicon: FileTypes | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> BrandingCustomization: + """Update branding customization. + + Partial update of the application's white-label branding. Send `application/json` to change colors, fonts, radii and UI options, or `multipart/form-data` to upload logo images. Only the fields you include are changed. Custom domain and white-label email cannot be set through this endpoint - use the Business Console for those. + + ``PATCH /v3/customization/`` + + Args: + color_primary: Headers / primary text color. + color_secondary: Paragraph / secondary text color. + color_background: Page background color. + color_panel: Panel / accent color. + color_panel_10: Panel color at 10% opacity. + color_on_panel_1: Support text color on panels. + color_on_panel_2: Pill / badge color on panels. + color_on_background: Text color on background. + color_button_1: Primary button color. + color_button_2: Secondary button color. + color_button_text_1: Primary button text color. + color_button_text_2: Secondary button text color. + color_pill_text: Text color for pills, inputs and selectors. + border_radius_panel: Panel corner radius in pixels. + border_radius_buttons: Button corner radius in pixels. + font_family: Google Font family name (e.g. `Inter`, `AR One Sans`). + font_weight: Font weight available for the selected family (e.g. `regular`, `500`, `700`). + app_public_name: Public name shown in the verification flow. + privacy_policy_url: Privacy policy URL shown to the end user. + skip_welcome_screen: Skip the initial welcome screen. + disable_login_with_didit: Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400). + hide_progress_bar: Hide the progress bar during the flow. + callback_seconds: Delay (seconds) before redirecting to the callback URL. + delete_logo_square: When `true`, removes the current square logo. + delete_logo_rectangular: When `true`, removes the current rectangular logo. + delete_favicon: When `true`, removes the current favicon. + image_square: Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. + image_rectangular: Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`. + image_favicon: Favicon image. Max **500 KB**. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "BrandingCustomization", + await self._client.request( + "patch", + "/v3/customization/", + cast_to=BrandingCustomization, + auth="api_key", + body={ + "color_primary": color_primary, + "color_secondary": color_secondary, + "color_background": color_background, + "color_panel": color_panel, + "color_panel_10": color_panel_10, + "color_on_panel_1": color_on_panel_1, + "color_on_panel_2": color_on_panel_2, + "color_on_background": color_on_background, + "color_button_1": color_button_1, + "color_button_2": color_button_2, + "color_button_text_1": color_button_text_1, + "color_button_text_2": color_button_text_2, + "color_pill_text": color_pill_text, + "border_radius_panel": border_radius_panel, + "border_radius_buttons": border_radius_buttons, + "font_family": font_family, + "font_weight": font_weight, + "app_public_name": app_public_name, + "privacy_policy_url": privacy_policy_url, + "skip_welcome_screen": skip_welcome_screen, + "disable_login_with_didit": disable_login_with_didit, + "hide_progress_bar": hide_progress_bar, + "callback_seconds": callback_seconds, + "delete_logo_square": delete_logo_square, + "delete_logo_rectangular": delete_logo_rectangular, + "delete_favicon": delete_favicon, + "image_square": image_square, + "image_rectangular": image_rectangular, + "image_favicon": image_favicon, + }, + multipart="auto", + file_fields=( + "image_favicon", + "image_rectangular", + "image_square", + ), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class CustomizationWithRawResponse: + def __init__(self, resource: Customization) -> None: + self._resource = resource + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncCustomizationWithRawResponse: + def __init__(self, resource: AsyncCustomization) -> None: + self._resource = resource + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.update = async_to_raw_response_wrapper(resource.update) + + +class CustomizationWithStreamingResponse: + def __init__(self, resource: Customization) -> None: + self._resource = resource + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncCustomizationWithStreamingResponse: + def __init__(self, resource: AsyncCustomization) -> None: + self._resource = resource + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/database_validation.py b/src/didit/resources/database_validation.py new file mode 100644 index 0000000..e6952f3 --- /dev/null +++ b/src/didit/resources/database_validation.py @@ -0,0 +1,602 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``database_validation`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.database_validation.validate_params import ( + DatabaseValidationValidateParamsAddressVariant1, +) +from didit.types.database_validation.validate_response import DatabaseValidationValidateResponse + +__all__ = [ + "DatabaseValidation", + "AsyncDatabaseValidation", + "DatabaseValidationWithRawResponse", + "AsyncDatabaseValidationWithRawResponse", + "DatabaseValidationWithStreamingResponse", + "AsyncDatabaseValidationWithStreamingResponse", +] + + +class DatabaseValidation(SyncAPIResource): + """The ``database_validation`` resource of the Didit API. + + Methods: ``validate``. + """ + + @cached_property + def with_raw_response(self) -> DatabaseValidationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return DatabaseValidationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DatabaseValidationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return DatabaseValidationWithStreamingResponse(self) + + def validate( + self, + *, + issuing_state: str, + services: Sequence[str] | NotGiven = NOT_GIVEN, + validation_type: str | NotGiven = NOT_GIVEN, + consent: bool | NotGiven = NOT_GIVEN, + identification_number: str | NotGiven = NOT_GIVEN, + first_name: str | NotGiven = NOT_GIVEN, + last_name: str | NotGiven = NOT_GIVEN, + middle_name: str | NotGiven = NOT_GIVEN, + full_name: str | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | NotGiven = NOT_GIVEN, + personal_number: str | NotGiven = NOT_GIVEN, + tax_number: str | NotGiven = NOT_GIVEN, + document_number: str | NotGiven = NOT_GIVEN, + document_type: Literal[ + "P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER" + ] + | str + | NotGiven = NOT_GIVEN, + expiration_date: str | _dt.date | NotGiven = NOT_GIVEN, + date_of_issue: str | _dt.date | NotGiven = NOT_GIVEN, + nationality: str | NotGiven = NOT_GIVEN, + gender: Literal["M", "F", "X"] | str | NotGiven = NOT_GIVEN, + address: DatabaseValidationValidateParamsAddressVariant1 | str | NotGiven = NOT_GIVEN, + address_element_1: str | NotGiven = NOT_GIVEN, + address_element_2: str | NotGiven = NOT_GIVEN, + address_element_3: str | NotGiven = NOT_GIVEN, + address_element_4: str | NotGiven = NOT_GIVEN, + address_element_5: str | NotGiven = NOT_GIVEN, + postal_code: str | NotGiven = NOT_GIVEN, + driver_license_number: str | NotGiven = NOT_GIVEN, + driver_license_card_number: str | NotGiven = NOT_GIVEN, + driver_license_state: str | NotGiven = NOT_GIVEN, + driver_license_version: str | NotGiven = NOT_GIVEN, + passport_number: str | NotGiven = NOT_GIVEN, + passport_expiration_date: str | _dt.date | NotGiven = NOT_GIVEN, + passport_file_number: str | NotGiven = NOT_GIVEN, + passport_issue_country: str | NotGiven = NOT_GIVEN, + medicare_card_number: str | NotGiven = NOT_GIVEN, + immi_card_number: str | NotGiven = NOT_GIVEN, + immi_card_expiry_date: str | _dt.date | NotGiven = NOT_GIVEN, + citizenship_certificate_number: str | NotGiven = NOT_GIVEN, + birth_registration_number: str | NotGiven = NOT_GIVEN, + birth_registration_date: str | _dt.date | NotGiven = NOT_GIVEN, + birth_registration_state: str | NotGiven = NOT_GIVEN, + marriage_certificate_number: str | NotGiven = NOT_GIVEN, + change_of_name_certificate_number: str | NotGiven = NOT_GIVEN, + first_partner_name: str | NotGiven = NOT_GIVEN, + last_partner_name: str | NotGiven = NOT_GIVEN, + voter_id: str | NotGiven = NOT_GIVEN, + epic_card: str | NotGiven = NOT_GIVEN, + pan: str | NotGiven = NOT_GIVEN, + national_id: str | NotGiven = NOT_GIVEN, + cic: str | NotGiven = NOT_GIVEN, + identificador_ciudadano: str | NotGiven = NOT_GIVEN, + ocr: str | NotGiven = NOT_GIVEN, + voter_number: str | NotGiven = NOT_GIVEN, + emission_number: str | NotGiven = NOT_GIVEN, + bvn: str | NotGiven = NOT_GIVEN, + bank_card_number: str | NotGiven = NOT_GIVEN, + ssn: str | NotGiven = NOT_GIVEN, + phone: str | NotGiven = NOT_GIVEN, + landline: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + country_of_residence: str | NotGiven = NOT_GIVEN, + partial_match_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + no_match_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + selfie: FileTypes | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DatabaseValidationValidateResponse: + """Database Validation (standalone). + + Validate a person's identity data against official government and registry sources - CPF in Brazil, RENAPER in Argentina, DNI registries in Spain and Peru, INE in Mexico, and 100+ more services across 60+ countries. + + **Service selection.** Each country exposes one or more catalog services identified by a `service_id` (e.g. `bra_cpf`, `arg_renaper`, `pan_cedula_sib`). Pin the services you want with the `services` array - if you omit it, exactly **one** default service runs (the longest-established live service for the country). Sending `services` also switches the response to the extended shape with `services_used` and `match_score`. Discover services, their required fields, and prices via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. Some services are flagged `requires_consent=true` in the catalog and demand `consent=true`; others require onboarding for your organization before they can be called. + + **Input fields.** `identification_number` is a universal field that maps to the right country-specific field automatically (see its description). Country format rules are enforced before any provider is called (e.g. BRA CPF must be exactly 11 digits) and format failures are never billed. Biometric services need a `selfie` upload (multipart only); Argentina RENAPER additionally requires `gender`. + + **Results.** Each service returns a field-by-field `validation` map plus a vendor-neutral `outcome_code` (`MATCH`, `PARTIAL_MATCH`, `NO_MATCH`, `DOCUMENT_NOT_FOUND`, `INVALID_DOCUMENT_FORMAT`, `INVALID_INPUT`, `MINOR_BLOCKED`, `DECEASED`, `BIOMETRIC_NO_MATCH`, `BIOMETRIC_IMAGE_UNUSABLE`, `INCONCLUSIVE`, `DOCUMENT_SUPERSEDED`, `REGISTRY_UNAVAILABLE`, `REGISTRY_ERROR`). Note the distinctions: `NO_MATCH` is a definitive mismatch, `INCONCLUSIVE` means the registry could not confirm either way (review, not decline), and `BIOMETRIC_IMAGE_UNUSABLE` is a technical selfie problem (retake), unlike the definitive `BIOMETRIC_NO_MATCH`. The overall `match_type` aggregates across services (any full → `full_match`, else any partial → `partial_match`, else `no_match`) and `no_match_action`/`partial_match_action` translate it into the final `status`. `validation_type` is derived: `two_by_two` when two or more distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions), else `one_by_one`. + + **Persistence.** `save_api_request` defaults to **true**: the result is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), appears in the console, and fires a `status.updated` webhook. It also controls the `validations` shape - per-service objects when saved, a merged `{field: match}` map when not. + + **Billing.** Per-service: each service that returns a billable result is charged its own catalog price. Failed or skipped services are not billed, and requests rejected with `400` cost nothing. The pre-flight balance check covers the sum of the selected services' prices (`403` when short). + + **Failures.** When no selected service returns a usable result, the API returns `400` with `validation_errors` if every unanswered attempt explicitly refused the input (`provider_rejected_input`, `retryable: false`). Correct the input before trying again. Otherwise it returns `502` with `validation_errors` (`empty_provider_response`, `retryable: true`), including mixed input rejection and unavailable fallback attempts; nothing is billed (a session is still recorded with status `Not Finished` when `save_api_request=true`). When only some services fail, the `200` response carries their failures in `database_validation.errors`. + + **Sandbox.** Keys from sandbox applications skip the registry calls and billing and return a static `full_match` response - the sandbox payload always includes `services_used` and `match_score: 100` regardless of how `services` was sent, so don't validate the live extended-shape gating or the 0.0–1.0 `match_score` fraction against sandbox responses. + + Send the request as `application/json`, or as `multipart/form-data` when uploading a `selfie`. + + ``POST /v3/database-validation/`` + + Args: + issuing_state: ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options. + services: Catalog `service_id`s to run for this country (e.g. `["bra_cpf"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** - the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. + validation_type: DEPRECATED. Accepted for backward compatibility but ignored - the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services. + consent: Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog - without it those services return `400` with `services_requiring_consent`. + identification_number: Universal identification number - automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field. + first_name: The individual's first name. Required by some services/countries. + last_name: The individual's last name. Required by some services/countries. + middle_name: Middle name, used by some country services (AUS, NZL, …). + full_name: Full name - some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name). + date_of_birth: Date of birth, `YYYY-MM-DD`. Required by many services. + personal_number: Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead. + tax_number: Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead. + document_number: Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead. + document_type: Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP). + expiration_date: Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation. + date_of_issue: Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation. + nationality: Nationality as ISO 3166-1 alpha-3. Required by some services. + gender: Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`). + address: Residential address. Prefer the structured object `{"street_1":"123 Main St","street_2":"Apt 4B","city":"Springfield","region":"IL","postal_code":"62701","country":"US"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`. + address_element_1: Street address including street number and type. If omitted, derived from `address`. + address_element_2: Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly. + address_element_3: City, suburb, district, locality, or neighborhood. If omitted, derived from `address`. + address_element_4: State, province, region, or town. If omitted, derived from `address`. + address_element_5: Postcode or postal code. `postal_code` is accepted as an alias. + postal_code: Postal code for address-based services (alias of `address_element_5`). + driver_license_number: Driver licence number for government document-verification services (AUS, NZL, IND). + driver_license_card_number: Physical driver licence card number, where required separately from the licence number (AUS). + driver_license_state: Driver licence state or territory of issue (AUS). + driver_license_version: Driver licence version code (NZL). + passport_number: Passport number for government passport-verification services (AUS, NZL, IND, CHN). + passport_expiration_date: Passport expiry date, `YYYY-MM-DD`, for passport-verification services. + passport_file_number: Passport file number (IND). + passport_issue_country: Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services. + medicare_card_number: Medicare card number (AUS). + immi_card_number: Australian ImmiCard number. + immi_card_expiry_date: Australian ImmiCard expiry date. + citizenship_certificate_number: Citizenship certificate number (AUS). + birth_registration_number: Birth-certificate registration number (AUS). + birth_registration_date: Birth-certificate registration date (AUS). + birth_registration_state: Birth-certificate registration state (AUS). + marriage_certificate_number: Marriage certificate number (AUS). + change_of_name_certificate_number: Change-of-name certificate number (AUS). + first_partner_name: First name of the partner on a marriage certificate (AUS). + last_partner_name: Last name of the partner on a marriage certificate (AUS). + voter_id: Voter registration number (IND EPIC, IRL). + epic_card: India EPIC voter card number. + pan: India PAN (Permanent Account Number). + national_id: National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …). + cic: Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`). + identificador_ciudadano: Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`. + ocr: Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`. + voter_number: Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. + emission_number: Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. + bvn: Nigerian Bank Verification Number. + bank_card_number: Bank card number (CHN bank-card verification). + ssn: Social Security Number (USA). + phone: Phone number for phone-verification services. + landline: Landline number for phone-verification services. + email: Email address for identity-verification services. + country_of_residence: ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup. + partial_match_action: What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning). + no_match_action: What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`. + save_api_request: Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`. + vendor_data: Your identifier for the validated user; echoed back and stored with the session. + metadata: Free-form JSON stored with the request and echoed back. + selfie: Selfie image (`jpg`, `jpeg`, `png`, `webp`; max 2 MB - images are compressed to ~1.5 MB before upload to the registry) for biometric validation services, e.g. Argentina RENAPER (`arg_renaper`) and Panama SIB (`pan_cedula_sib`, `pan_cedula_sib_plus`). Required whenever a selected service lists it as a required field. Only available via `multipart/form-data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DatabaseValidationValidateResponse", + self._client.request( + "post", + "/v3/database-validation/", + cast_to=DatabaseValidationValidateResponse, + auth="api_key", + body={ + "issuing_state": issuing_state, + "services": services, + "validation_type": validation_type, + "consent": consent, + "identification_number": identification_number, + "first_name": first_name, + "last_name": last_name, + "middle_name": middle_name, + "full_name": full_name, + "date_of_birth": date_of_birth, + "personal_number": personal_number, + "tax_number": tax_number, + "document_number": document_number, + "document_type": document_type, + "expiration_date": expiration_date, + "date_of_issue": date_of_issue, + "nationality": nationality, + "gender": gender, + "address": address, + "address_element_1": address_element_1, + "address_element_2": address_element_2, + "address_element_3": address_element_3, + "address_element_4": address_element_4, + "address_element_5": address_element_5, + "postal_code": postal_code, + "driver_license_number": driver_license_number, + "driver_license_card_number": driver_license_card_number, + "driver_license_state": driver_license_state, + "driver_license_version": driver_license_version, + "passport_number": passport_number, + "passport_expiration_date": passport_expiration_date, + "passport_file_number": passport_file_number, + "passport_issue_country": passport_issue_country, + "medicare_card_number": medicare_card_number, + "immi_card_number": immi_card_number, + "immi_card_expiry_date": immi_card_expiry_date, + "citizenship_certificate_number": citizenship_certificate_number, + "birth_registration_number": birth_registration_number, + "birth_registration_date": birth_registration_date, + "birth_registration_state": birth_registration_state, + "marriage_certificate_number": marriage_certificate_number, + "change_of_name_certificate_number": change_of_name_certificate_number, + "first_partner_name": first_partner_name, + "last_partner_name": last_partner_name, + "voter_id": voter_id, + "epic_card": epic_card, + "pan": pan, + "national_id": national_id, + "cic": cic, + "identificador_ciudadano": identificador_ciudadano, + "ocr": ocr, + "voter_number": voter_number, + "emission_number": emission_number, + "bvn": bvn, + "bank_card_number": bank_card_number, + "ssn": ssn, + "phone": phone, + "landline": landline, + "email": email, + "country_of_residence": country_of_residence, + "partial_match_action": partial_match_action, + "no_match_action": no_match_action, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + "selfie": selfie, + }, + multipart="auto", + file_fields=("selfie",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncDatabaseValidation(AsyncAPIResource): + """The ``database_validation`` resource of the Didit API. + + Methods: ``validate``. + """ + + @cached_property + def with_raw_response(self) -> AsyncDatabaseValidationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncDatabaseValidationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDatabaseValidationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncDatabaseValidationWithStreamingResponse(self) + + async def validate( + self, + *, + issuing_state: str, + services: Sequence[str] | NotGiven = NOT_GIVEN, + validation_type: str | NotGiven = NOT_GIVEN, + consent: bool | NotGiven = NOT_GIVEN, + identification_number: str | NotGiven = NOT_GIVEN, + first_name: str | NotGiven = NOT_GIVEN, + last_name: str | NotGiven = NOT_GIVEN, + middle_name: str | NotGiven = NOT_GIVEN, + full_name: str | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | NotGiven = NOT_GIVEN, + personal_number: str | NotGiven = NOT_GIVEN, + tax_number: str | NotGiven = NOT_GIVEN, + document_number: str | NotGiven = NOT_GIVEN, + document_type: Literal[ + "P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER" + ] + | str + | NotGiven = NOT_GIVEN, + expiration_date: str | _dt.date | NotGiven = NOT_GIVEN, + date_of_issue: str | _dt.date | NotGiven = NOT_GIVEN, + nationality: str | NotGiven = NOT_GIVEN, + gender: Literal["M", "F", "X"] | str | NotGiven = NOT_GIVEN, + address: DatabaseValidationValidateParamsAddressVariant1 | str | NotGiven = NOT_GIVEN, + address_element_1: str | NotGiven = NOT_GIVEN, + address_element_2: str | NotGiven = NOT_GIVEN, + address_element_3: str | NotGiven = NOT_GIVEN, + address_element_4: str | NotGiven = NOT_GIVEN, + address_element_5: str | NotGiven = NOT_GIVEN, + postal_code: str | NotGiven = NOT_GIVEN, + driver_license_number: str | NotGiven = NOT_GIVEN, + driver_license_card_number: str | NotGiven = NOT_GIVEN, + driver_license_state: str | NotGiven = NOT_GIVEN, + driver_license_version: str | NotGiven = NOT_GIVEN, + passport_number: str | NotGiven = NOT_GIVEN, + passport_expiration_date: str | _dt.date | NotGiven = NOT_GIVEN, + passport_file_number: str | NotGiven = NOT_GIVEN, + passport_issue_country: str | NotGiven = NOT_GIVEN, + medicare_card_number: str | NotGiven = NOT_GIVEN, + immi_card_number: str | NotGiven = NOT_GIVEN, + immi_card_expiry_date: str | _dt.date | NotGiven = NOT_GIVEN, + citizenship_certificate_number: str | NotGiven = NOT_GIVEN, + birth_registration_number: str | NotGiven = NOT_GIVEN, + birth_registration_date: str | _dt.date | NotGiven = NOT_GIVEN, + birth_registration_state: str | NotGiven = NOT_GIVEN, + marriage_certificate_number: str | NotGiven = NOT_GIVEN, + change_of_name_certificate_number: str | NotGiven = NOT_GIVEN, + first_partner_name: str | NotGiven = NOT_GIVEN, + last_partner_name: str | NotGiven = NOT_GIVEN, + voter_id: str | NotGiven = NOT_GIVEN, + epic_card: str | NotGiven = NOT_GIVEN, + pan: str | NotGiven = NOT_GIVEN, + national_id: str | NotGiven = NOT_GIVEN, + cic: str | NotGiven = NOT_GIVEN, + identificador_ciudadano: str | NotGiven = NOT_GIVEN, + ocr: str | NotGiven = NOT_GIVEN, + voter_number: str | NotGiven = NOT_GIVEN, + emission_number: str | NotGiven = NOT_GIVEN, + bvn: str | NotGiven = NOT_GIVEN, + bank_card_number: str | NotGiven = NOT_GIVEN, + ssn: str | NotGiven = NOT_GIVEN, + phone: str | NotGiven = NOT_GIVEN, + landline: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + country_of_residence: str | NotGiven = NOT_GIVEN, + partial_match_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + no_match_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + selfie: FileTypes | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DatabaseValidationValidateResponse: + """Database Validation (standalone). + + Validate a person's identity data against official government and registry sources - CPF in Brazil, RENAPER in Argentina, DNI registries in Spain and Peru, INE in Mexico, and 100+ more services across 60+ countries. + + **Service selection.** Each country exposes one or more catalog services identified by a `service_id` (e.g. `bra_cpf`, `arg_renaper`, `pan_cedula_sib`). Pin the services you want with the `services` array - if you omit it, exactly **one** default service runs (the longest-established live service for the country). Sending `services` also switches the response to the extended shape with `services_used` and `match_score`. Discover services, their required fields, and prices via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. Some services are flagged `requires_consent=true` in the catalog and demand `consent=true`; others require onboarding for your organization before they can be called. + + **Input fields.** `identification_number` is a universal field that maps to the right country-specific field automatically (see its description). Country format rules are enforced before any provider is called (e.g. BRA CPF must be exactly 11 digits) and format failures are never billed. Biometric services need a `selfie` upload (multipart only); Argentina RENAPER additionally requires `gender`. + + **Results.** Each service returns a field-by-field `validation` map plus a vendor-neutral `outcome_code` (`MATCH`, `PARTIAL_MATCH`, `NO_MATCH`, `DOCUMENT_NOT_FOUND`, `INVALID_DOCUMENT_FORMAT`, `INVALID_INPUT`, `MINOR_BLOCKED`, `DECEASED`, `BIOMETRIC_NO_MATCH`, `BIOMETRIC_IMAGE_UNUSABLE`, `INCONCLUSIVE`, `DOCUMENT_SUPERSEDED`, `REGISTRY_UNAVAILABLE`, `REGISTRY_ERROR`). Note the distinctions: `NO_MATCH` is a definitive mismatch, `INCONCLUSIVE` means the registry could not confirm either way (review, not decline), and `BIOMETRIC_IMAGE_UNUSABLE` is a technical selfie problem (retake), unlike the definitive `BIOMETRIC_NO_MATCH`. The overall `match_type` aggregates across services (any full → `full_match`, else any partial → `partial_match`, else `no_match`) and `no_match_action`/`partial_match_action` translate it into the final `status`. `validation_type` is derived: `two_by_two` when two or more distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions), else `one_by_one`. + + **Persistence.** `save_api_request` defaults to **true**: the result is stored as a session (`request_id` works with `GET /v3/session/{sessionId}/decision/`), appears in the console, and fires a `status.updated` webhook. It also controls the `validations` shape - per-service objects when saved, a merged `{field: match}` map when not. + + **Billing.** Per-service: each service that returns a billable result is charged its own catalog price. Failed or skipped services are not billed, and requests rejected with `400` cost nothing. The pre-flight balance check covers the sum of the selected services' prices (`403` when short). + + **Failures.** When no selected service returns a usable result, the API returns `400` with `validation_errors` if every unanswered attempt explicitly refused the input (`provider_rejected_input`, `retryable: false`). Correct the input before trying again. Otherwise it returns `502` with `validation_errors` (`empty_provider_response`, `retryable: true`), including mixed input rejection and unavailable fallback attempts; nothing is billed (a session is still recorded with status `Not Finished` when `save_api_request=true`). When only some services fail, the `200` response carries their failures in `database_validation.errors`. + + **Sandbox.** Keys from sandbox applications skip the registry calls and billing and return a static `full_match` response - the sandbox payload always includes `services_used` and `match_score: 100` regardless of how `services` was sent, so don't validate the live extended-shape gating or the 0.0–1.0 `match_score` fraction against sandbox responses. + + Send the request as `application/json`, or as `multipart/form-data` when uploading a `selfie`. + + ``POST /v3/database-validation/`` + + Args: + issuing_state: ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options. + services: Catalog `service_id`s to run for this country (e.g. `["bra_cpf"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** - the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor. + validation_type: DEPRECATED. Accepted for backward compatibility but ignored - the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services. + consent: Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog - without it those services return `400` with `services_requiring_consent`. + identification_number: Universal identification number - automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field. + first_name: The individual's first name. Required by some services/countries. + last_name: The individual's last name. Required by some services/countries. + middle_name: Middle name, used by some country services (AUS, NZL, …). + full_name: Full name - some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name). + date_of_birth: Date of birth, `YYYY-MM-DD`. Required by many services. + personal_number: Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead. + tax_number: Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead. + document_number: Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead. + document_type: Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP). + expiration_date: Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation. + date_of_issue: Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation. + nationality: Nationality as ISO 3166-1 alpha-3. Required by some services. + gender: Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`). + address: Residential address. Prefer the structured object `{"street_1":"123 Main St","street_2":"Apt 4B","city":"Springfield","region":"IL","postal_code":"62701","country":"US"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`. + address_element_1: Street address including street number and type. If omitted, derived from `address`. + address_element_2: Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly. + address_element_3: City, suburb, district, locality, or neighborhood. If omitted, derived from `address`. + address_element_4: State, province, region, or town. If omitted, derived from `address`. + address_element_5: Postcode or postal code. `postal_code` is accepted as an alias. + postal_code: Postal code for address-based services (alias of `address_element_5`). + driver_license_number: Driver licence number for government document-verification services (AUS, NZL, IND). + driver_license_card_number: Physical driver licence card number, where required separately from the licence number (AUS). + driver_license_state: Driver licence state or territory of issue (AUS). + driver_license_version: Driver licence version code (NZL). + passport_number: Passport number for government passport-verification services (AUS, NZL, IND, CHN). + passport_expiration_date: Passport expiry date, `YYYY-MM-DD`, for passport-verification services. + passport_file_number: Passport file number (IND). + passport_issue_country: Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services. + medicare_card_number: Medicare card number (AUS). + immi_card_number: Australian ImmiCard number. + immi_card_expiry_date: Australian ImmiCard expiry date. + citizenship_certificate_number: Citizenship certificate number (AUS). + birth_registration_number: Birth-certificate registration number (AUS). + birth_registration_date: Birth-certificate registration date (AUS). + birth_registration_state: Birth-certificate registration state (AUS). + marriage_certificate_number: Marriage certificate number (AUS). + change_of_name_certificate_number: Change-of-name certificate number (AUS). + first_partner_name: First name of the partner on a marriage certificate (AUS). + last_partner_name: Last name of the partner on a marriage certificate (AUS). + voter_id: Voter registration number (IND EPIC, IRL). + epic_card: India EPIC voter card number. + pan: India PAN (Permanent Account Number). + national_id: National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …). + cic: Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`). + identificador_ciudadano: Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`. + ocr: Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`. + voter_number: Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. + emission_number: Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`. + bvn: Nigerian Bank Verification Number. + bank_card_number: Bank card number (CHN bank-card verification). + ssn: Social Security Number (USA). + phone: Phone number for phone-verification services. + landline: Landline number for phone-verification services. + email: Email address for identity-verification services. + country_of_residence: ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup. + partial_match_action: What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning). + no_match_action: What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`. + save_api_request: Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`. + vendor_data: Your identifier for the validated user; echoed back and stored with the session. + metadata: Free-form JSON stored with the request and echoed back. + selfie: Selfie image (`jpg`, `jpeg`, `png`, `webp`; max 2 MB - images are compressed to ~1.5 MB before upload to the registry) for biometric validation services, e.g. Argentina RENAPER (`arg_renaper`) and Panama SIB (`pan_cedula_sib`, `pan_cedula_sib_plus`). Required whenever a selected service lists it as a required field. Only available via `multipart/form-data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DatabaseValidationValidateResponse", + await self._client.request( + "post", + "/v3/database-validation/", + cast_to=DatabaseValidationValidateResponse, + auth="api_key", + body={ + "issuing_state": issuing_state, + "services": services, + "validation_type": validation_type, + "consent": consent, + "identification_number": identification_number, + "first_name": first_name, + "last_name": last_name, + "middle_name": middle_name, + "full_name": full_name, + "date_of_birth": date_of_birth, + "personal_number": personal_number, + "tax_number": tax_number, + "document_number": document_number, + "document_type": document_type, + "expiration_date": expiration_date, + "date_of_issue": date_of_issue, + "nationality": nationality, + "gender": gender, + "address": address, + "address_element_1": address_element_1, + "address_element_2": address_element_2, + "address_element_3": address_element_3, + "address_element_4": address_element_4, + "address_element_5": address_element_5, + "postal_code": postal_code, + "driver_license_number": driver_license_number, + "driver_license_card_number": driver_license_card_number, + "driver_license_state": driver_license_state, + "driver_license_version": driver_license_version, + "passport_number": passport_number, + "passport_expiration_date": passport_expiration_date, + "passport_file_number": passport_file_number, + "passport_issue_country": passport_issue_country, + "medicare_card_number": medicare_card_number, + "immi_card_number": immi_card_number, + "immi_card_expiry_date": immi_card_expiry_date, + "citizenship_certificate_number": citizenship_certificate_number, + "birth_registration_number": birth_registration_number, + "birth_registration_date": birth_registration_date, + "birth_registration_state": birth_registration_state, + "marriage_certificate_number": marriage_certificate_number, + "change_of_name_certificate_number": change_of_name_certificate_number, + "first_partner_name": first_partner_name, + "last_partner_name": last_partner_name, + "voter_id": voter_id, + "epic_card": epic_card, + "pan": pan, + "national_id": national_id, + "cic": cic, + "identificador_ciudadano": identificador_ciudadano, + "ocr": ocr, + "voter_number": voter_number, + "emission_number": emission_number, + "bvn": bvn, + "bank_card_number": bank_card_number, + "ssn": ssn, + "phone": phone, + "landline": landline, + "email": email, + "country_of_residence": country_of_residence, + "partial_match_action": partial_match_action, + "no_match_action": no_match_action, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + "selfie": selfie, + }, + multipart="auto", + file_fields=("selfie",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class DatabaseValidationWithRawResponse: + def __init__(self, resource: DatabaseValidation) -> None: + self._resource = resource + self.validate = to_raw_response_wrapper(resource.validate) + + +class AsyncDatabaseValidationWithRawResponse: + def __init__(self, resource: AsyncDatabaseValidation) -> None: + self._resource = resource + self.validate = async_to_raw_response_wrapper(resource.validate) + + +class DatabaseValidationWithStreamingResponse: + def __init__(self, resource: DatabaseValidation) -> None: + self._resource = resource + self.validate = to_streamed_response_wrapper(resource.validate) + + +class AsyncDatabaseValidationWithStreamingResponse: + def __init__(self, resource: AsyncDatabaseValidation) -> None: + self._resource = resource + self.validate = async_to_streamed_response_wrapper(resource.validate) diff --git a/src/didit/resources/document_ai.py b/src/didit/resources/document_ai.py new file mode 100644 index 0000000..39f8467 --- /dev/null +++ b/src/didit/resources/document_ai.py @@ -0,0 +1,296 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``document_ai`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.document_ai.analyze_response import DocumentAiAnalyzeResponse + +__all__ = [ + "DocumentAi", + "AsyncDocumentAi", + "DocumentAiWithRawResponse", + "AsyncDocumentAiWithRawResponse", + "DocumentAiWithStreamingResponse", + "AsyncDocumentAiWithStreamingResponse", +] + + +class DocumentAi(SyncAPIResource): + """The ``document_ai`` resource of the Didit API. + + Methods: ``analyze``. + """ + + @cached_property + def with_raw_response(self) -> DocumentAiWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return DocumentAiWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> DocumentAiWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return DocumentAiWithStreamingResponse(self) + + def analyze( + self, + *, + document: FileTypes, + fields: str, + expected_first_name: str | NotGiven = NOT_GIVEN, + expected_last_name: str | NotGiven = NOT_GIVEN, + expected_company_name: str | NotGiven = NOT_GIVEN, + expected_company_country: str | NotGiven = NOT_GIVEN, + document_ai_name_match_score_threshold: int | NotGiven = NOT_GIVEN, + document_ai_unreadable_document_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_missing_required_fields_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_document_tampering_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_name_mismatch_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_unsupported_file_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DocumentAiAnalyzeResponse: + """Document AI (custom field extraction + tamper checks). + + Extract the fields **you** define from any supporting document - payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices - in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection. + + **Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes. + + **Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` - `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name. + + **Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) - not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`. + + **Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: "Declined"`, `REVIEW` → `status: "In Review"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` - always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code. + + **Billing.** Each `200` response consumes one Document AI API credit - **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged. + + **Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`. + + ``POST /v3/document-ai/`` + + Args: + document: The document to read. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`, `zip`. Maximum upload size: **30 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs are rejected with `400`. Images are automatically compressed before processing. + fields: One JSON-encoded array string with **1–30** field definitions (maximum 16 KB). Each item: `key` (required, `^[a-z0-9_]{1,64}$`, unique - the key returned in `extracted_data`), `name` (≤100 chars, shown in messages), `instruction` (≤500 chars, what the model should read), `type` (`text` default | `number` | `date`), `required` (boolean, default `false`), `is_full_name` (boolean, default `false`, at most one per request). Bracket-style form arrays, invalid JSON, duplicate keys, or more than one `is_full_name` return `400` before any processing or billing. + expected_first_name: First name to verify against the document's name field (fuzzy match, transliteration-aware). Use together with `expected_last_name`; cannot be combined with `expected_company_name`. + expected_last_name: Last name to verify against the document's name field, combined with `expected_first_name` for `name_match_score`. + expected_company_name: Company name to verify against the document's name field (for business documents). Cannot be combined with `expected_first_name` / `expected_last_name` (no example value on purpose - the generated request sample uses the person fields; swap them for this one when verifying a business document). + expected_company_country: Optional ISO 3166-1 alpha-2 country code (two ASCII letters, e.g. `FR`) that tunes company-name normalization for `expected_company_name`. Only used with `expected_company_name`. + document_ai_name_match_score_threshold: Minimum `name_match_score` (0–100) to consider the name matched. A lower score adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`. + document_ai_unreadable_document_action: What to do when no field could be read from the document (`DOCUMENT_AI_UNREADABLE_DOCUMENT`). + document_ai_missing_required_fields_action: What to do when at least one `required` field could not be read (`DOCUMENT_AI_MISSING_REQUIRED_FIELDS`; `additional_data.missing_fields` lists them). + document_ai_document_tampering_action: What to do when PDF/EXIF forensics suspect manipulation - modified after digital signing, known PDF editor, overlay-text manipulation, suspicious re-export, image anomalies (`DOCUMENT_AI_SUSPECTED_MANIPULATION`). + document_ai_name_mismatch_action: What to do when `name_match_score` is below the threshold (`DOCUMENT_AI_NAME_MISMATCH`). + document_ai_unsupported_file_action: What to do when the upload is empty or not a processable file (`DOCUMENT_AI_UNSUPPORTED_FILE`). + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, with the uploaded document, the extracted values, the field definitions and the name-match snapshot stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only; the call is still billed. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DocumentAiAnalyzeResponse", + self._client.request( + "post", + "/v3/document-ai/", + cast_to=DocumentAiAnalyzeResponse, + auth="api_key", + body={ + "document": document, + "fields": fields, + "expected_first_name": expected_first_name, + "expected_last_name": expected_last_name, + "expected_company_name": expected_company_name, + "expected_company_country": expected_company_country, + "document_ai_name_match_score_threshold": document_ai_name_match_score_threshold, + "document_ai_unreadable_document_action": document_ai_unreadable_document_action, + "document_ai_missing_required_fields_action": document_ai_missing_required_fields_action, + "document_ai_document_tampering_action": document_ai_document_tampering_action, + "document_ai_name_mismatch_action": document_ai_name_mismatch_action, + "document_ai_unsupported_file_action": document_ai_unsupported_file_action, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("document",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncDocumentAi(AsyncAPIResource): + """The ``document_ai`` resource of the Didit API. + + Methods: ``analyze``. + """ + + @cached_property + def with_raw_response(self) -> AsyncDocumentAiWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncDocumentAiWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncDocumentAiWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncDocumentAiWithStreamingResponse(self) + + async def analyze( + self, + *, + document: FileTypes, + fields: str, + expected_first_name: str | NotGiven = NOT_GIVEN, + expected_last_name: str | NotGiven = NOT_GIVEN, + expected_company_name: str | NotGiven = NOT_GIVEN, + expected_company_country: str | NotGiven = NOT_GIVEN, + document_ai_name_match_score_threshold: int | NotGiven = NOT_GIVEN, + document_ai_unreadable_document_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_missing_required_fields_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_document_tampering_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_name_mismatch_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + document_ai_unsupported_file_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DocumentAiAnalyzeResponse: + """Document AI (custom field extraction + tamper checks). + + Extract the fields **you** define from any supporting document - payslips, proof of funds, source-of-wealth letters, tax statements, contracts, invoices - in one call. Send the file plus a `fields` definition (key, name, extraction instruction, type) and get back typed values (`text`, `number`, `date` as `YYYY-MM-DD`), an optional name-match score against the expected person or company, PDF/EXIF tamper signals, an `Approved`/`Declined`/`In Review` `status`, and a `warnings` list explaining every issue found. This is the server-to-server counterpart of the hosted Document AI workflow step (see `/core-technology/document-ai/overview`); it uses Didit's default extraction model and does not expose provider selection. + + **Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes. + + **Request format.** `multipart/form-data` only. `fields` is **one JSON-encoded array string** (1–30 items, at most 16 KB); bracket-style form arrays are rejected with `400`. Each item is `{key, name, instruction, type, required, is_full_name}` - `key` is the JSON key you get back (`^[a-z0-9_]{1,64}$`, unique), `instruction` tells the model what to read, `type` is `text` (default) | `number` | `date`, `required` raises `DOCUMENT_AI_MISSING_REQUIRED_FIELDS` when the value cannot be read, and at most one field may set `is_full_name: true` to mark the value compared against the expected name. + + **Name matching.** Pass `expected_first_name` + `expected_last_name` (person) **or** `expected_company_name` (+ optional `expected_company_country`) - not both. The value of the `is_full_name` field (or, when none is marked, the first text field whose key looks like a name) is compared fuzzily; `name_match_score` (0–100) and `name_on_document` are always returned when both sides exist, and a score below `document_ai_name_match_score_threshold` (default 80) adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`. + + **Decision logic.** Each risk maps to a configurable action: `DECLINE` → `status: "Declined"`, `REVIEW` → `status: "In Review"`, `NO_ACTION` → informational only. Defaults mirror the workflow step: unreadable document `REVIEW`, missing required fields `REVIEW`, suspected manipulation `DECLINE`, name mismatch `NO_ACTION`, unsupported file `REVIEW`. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `Declined`/`In Review` - always inspect `document_ai.status` and `document_ai.warnings`, not just the HTTP code. + + **Billing.** Each `200` response consumes one Document AI API credit - **$0.20 per document, no free tier** (standalone APIs never have one). When the organization's balance cannot cover the call, the endpoint returns `403` before any processing. Sandbox API keys return a deterministic mock response and are not charged. + + **Persistence.** With `save_api_request=true` (default) the call is stored as an API-type session: the document, the extracted values, the field definitions and the name-match snapshot are kept, the session shows up in the Business Console, and `request_id` works with `GET /v3/session/{sessionId}/decision/`. + + ``POST /v3/document-ai/`` + + Args: + document: The document to read. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`, `zip`. Maximum upload size: **30 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs are rejected with `400`. Images are automatically compressed before processing. + fields: One JSON-encoded array string with **1–30** field definitions (maximum 16 KB). Each item: `key` (required, `^[a-z0-9_]{1,64}$`, unique - the key returned in `extracted_data`), `name` (≤100 chars, shown in messages), `instruction` (≤500 chars, what the model should read), `type` (`text` default | `number` | `date`), `required` (boolean, default `false`), `is_full_name` (boolean, default `false`, at most one per request). Bracket-style form arrays, invalid JSON, duplicate keys, or more than one `is_full_name` return `400` before any processing or billing. + expected_first_name: First name to verify against the document's name field (fuzzy match, transliteration-aware). Use together with `expected_last_name`; cannot be combined with `expected_company_name`. + expected_last_name: Last name to verify against the document's name field, combined with `expected_first_name` for `name_match_score`. + expected_company_name: Company name to verify against the document's name field (for business documents). Cannot be combined with `expected_first_name` / `expected_last_name` (no example value on purpose - the generated request sample uses the person fields; swap them for this one when verifying a business document). + expected_company_country: Optional ISO 3166-1 alpha-2 country code (two ASCII letters, e.g. `FR`) that tunes company-name normalization for `expected_company_name`. Only used with `expected_company_name`. + document_ai_name_match_score_threshold: Minimum `name_match_score` (0–100) to consider the name matched. A lower score adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`. + document_ai_unreadable_document_action: What to do when no field could be read from the document (`DOCUMENT_AI_UNREADABLE_DOCUMENT`). + document_ai_missing_required_fields_action: What to do when at least one `required` field could not be read (`DOCUMENT_AI_MISSING_REQUIRED_FIELDS`; `additional_data.missing_fields` lists them). + document_ai_document_tampering_action: What to do when PDF/EXIF forensics suspect manipulation - modified after digital signing, known PDF editor, overlay-text manipulation, suspicious re-export, image anomalies (`DOCUMENT_AI_SUSPECTED_MANIPULATION`). + document_ai_name_mismatch_action: What to do when `name_match_score` is below the threshold (`DOCUMENT_AI_NAME_MISMATCH`). + document_ai_unsupported_file_action: What to do when the upload is empty or not a processable file (`DOCUMENT_AI_UNSUPPORTED_FILE`). + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, with the uploaded document, the extracted values, the field definitions and the name-match snapshot stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only; the call is still billed. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DocumentAiAnalyzeResponse", + await self._client.request( + "post", + "/v3/document-ai/", + cast_to=DocumentAiAnalyzeResponse, + auth="api_key", + body={ + "document": document, + "fields": fields, + "expected_first_name": expected_first_name, + "expected_last_name": expected_last_name, + "expected_company_name": expected_company_name, + "expected_company_country": expected_company_country, + "document_ai_name_match_score_threshold": document_ai_name_match_score_threshold, + "document_ai_unreadable_document_action": document_ai_unreadable_document_action, + "document_ai_missing_required_fields_action": document_ai_missing_required_fields_action, + "document_ai_document_tampering_action": document_ai_document_tampering_action, + "document_ai_name_mismatch_action": document_ai_name_mismatch_action, + "document_ai_unsupported_file_action": document_ai_unsupported_file_action, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("document",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class DocumentAiWithRawResponse: + def __init__(self, resource: DocumentAi) -> None: + self._resource = resource + self.analyze = to_raw_response_wrapper(resource.analyze) + + +class AsyncDocumentAiWithRawResponse: + def __init__(self, resource: AsyncDocumentAi) -> None: + self._resource = resource + self.analyze = async_to_raw_response_wrapper(resource.analyze) + + +class DocumentAiWithStreamingResponse: + def __init__(self, resource: DocumentAi) -> None: + self._resource = resource + self.analyze = to_streamed_response_wrapper(resource.analyze) + + +class AsyncDocumentAiWithStreamingResponse: + def __init__(self, resource: AsyncDocumentAi) -> None: + self._resource = resource + self.analyze = async_to_streamed_response_wrapper(resource.analyze) diff --git a/src/didit/resources/email.py b/src/didit/resources/email.py new file mode 100644 index 0000000..cf04546 --- /dev/null +++ b/src/didit/resources/email.py @@ -0,0 +1,529 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``email`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.email_risk_response import EmailRiskResponse +from didit.types.components.email_verification_check_response import EmailVerificationCheckResponse +from didit.types.components.email_verification_send_response import EmailVerificationSendResponse +from didit.types.components.risk_signals import RiskSignals +from didit.types.email.send_code_params import ( + EmailSendCodeParamsOptions, + EmailSendCodeParamsSignals, +) + +__all__ = [ + "Email", + "AsyncEmail", + "EmailWithRawResponse", + "AsyncEmailWithRawResponse", + "EmailWithStreamingResponse", + "AsyncEmailWithStreamingResponse", +] + + +class Email(SyncAPIResource): + """The ``email`` resource of the Didit API. + + Methods: ``assess_risk``, ``check_code``, ``send_code``. + """ + + @cached_property + def with_raw_response(self) -> EmailWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return EmailWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> EmailWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return EmailWithStreamingResponse(self) + + def assess_risk( + self, + *, + email: str, + country_code: str | NotGiven = NOT_GIVEN, + signals: RiskSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EmailRiskResponse: + """Risk-score an email address. + + Return deliverability, disposable-address, breach, cross-session match, and normalized risk-intelligence signals without sending an OTP. The endpoint creates an API session and returns its id as `request_id`. + + **Decision.** The top-level `status` mirrors `email.status`. It is normally `Approved`; the risk evaluation can produce `In Review` or `Declined`. + + **Compatibility aliases.** Live responses expose the normalized intelligence object as both `email.email_intelligence` and `email.enrichment`. The values are identical. New integrations can read `email.enrichment`. Both keys are absent when no usable intelligence data was returned. + + **Billing.** Live calls use Email Risk API pricing. The request is billable once the lookup starts, even when some best-effort fields are unavailable. + + **Sandbox.** Sandbox keys validate the request and return deterministic approved data without sending an email, consuming credits, creating a session, or contributing to Networks. + + **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + + **Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price. + + ``POST /v3/email/risk/`` + + Args: + email: Email address to score. + country_code: Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage. + signals: Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks. + vendor_data: Your stable reference for this request. Stored on the API session and echoed in the response. + metadata: Your non-sensitive JSON metadata. Stored on the API session and echoed in the response. + enable_social: When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EmailRiskResponse", + self._client.request( + "post", + "/v3/email/risk/", + cast_to=EmailRiskResponse, + auth="api_key", + body={ + "email": email, + "country_code": country_code, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + "enable_social": enable_social, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def send_code( + self, + *, + email: str, + options: EmailSendCodeParamsOptions | NotGiven = NOT_GIVEN, + signals: EmailSendCodeParamsSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EmailVerificationSendResponse: + """Send Email Code. + + Send a one-time passcode (OTP) to an email address, then verify it with [`POST /v3/email/check/`](/standalone-apis/email-check). + + **How send and check pair up.** Verification state is keyed by your application plus the `email` address - the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same address and the code the user received before the window closes. Calling send again for the same address while a verification is pending generates a fresh code for that same verification (`status: "Retry"`, same `request_id`); the previous code stops working. At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries. + + **Deliverability pre-check.** Before anything is sent, the address goes through syntax and DNS/MX validation. Addresses that cannot receive mail return `200` with `status: "Undeliverable"` and `reason: "email_can_not_be_delivered"` - no email is sent, nothing is billed, and the verification is immediately finalized as `Declined` (a follow-up check returns `Expired or Not Found`). A downstream send failure reports the same way. + + **Code format and branding.** Codes are 4–8 characters (`options.code_size`, default 6), numeric by default; set `options.alphanumeric_code: true` for uppercase letters and digits (the check comparison is case-insensitive). The email template is localized via `options.locale` (54 supported languages) and can use your application's white-label branding via `options.use_white_label_customization`. + + **Billing.** One Email Verification API credit per successful send (`status: "Success"`), charged at send time. `Retry` and `Undeliverable` sends are free, and checks are free. + + **Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses. + + **Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no email is sent and nothing is persisted. Use code `123456` on the sandbox check. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/email/send/`` + + Args: + email: Recipient email address. Malformed addresses return `400`; syntactically valid addresses that cannot receive mail (failed DNS/MX validation) return `200` with `status: "Undeliverable"`. + options: OTP format, localization, and branding options. All fields are optional. + signals: Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional. + vendor_data: Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record. + metadata: Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EmailVerificationSendResponse", + self._client.request( + "post", + "/v3/email/send/", + cast_to=EmailVerificationSendResponse, + auth="api_key", + body={ + "email": email, + "options": options, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def check_code( + self, + *, + email: str, + code: str, + duplicated_email_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + breached_email_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + disposable_email_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + email_no_social_presence_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EmailVerificationCheckResponse: + """Check Email Code. + + Verify the OTP delivered by [`POST /v3/email/send/`](/standalone-apis/email-send) and get the final verification result plus email intelligence: deliverability, breach exposure (`is_breached` and the `breaches` list from a breach-intelligence database), disposable-provider detection, and duplicate usage of the address across your sessions. + + **How the check finds the verification.** Matching is by your application plus the `email` address - `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, undeliverable at send time, or older than 5 minutes) the endpoint returns `200` with `status: "Expired or Not Found"`. + + **Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: "Failed"` with the attempts remaining and `email: null`; the third wrong code finalizes the verification as `Declined` with an `EMAIL_CODE_ATTEMPTS_EXCEEDED` warning and returns the full email report. Codes are compared case-insensitively (relevant for `alphanumeric_code` sends); only the most recently sent code is valid. + + **Outcomes.** `Approved` - correct code and no declining risk. `Declined` - terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted address, or the address found undeliverable at finalization), or the attempt budget was exhausted. `Failed` - wrong code, attempts remaining. `Expired or Not Found` - nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `email` carries the full report (`is_breached`, `breaches`, `is_disposable`, `is_undeliverable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID. + + **Risk actions.** `duplicated_email_action`, `breached_email_action`, and `disposable_email_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `email.warnings` without affecting the status. + + **Billing.** Checks are free - the credit is consumed by the successful send. + + **Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook. + + **Sandbox.** Sandbox API keys skip all processing: any well-formed code (`123456` included) returns a static `Approved` payload with a simplified `email` object (`status`, `email`, `is_breached`, `is_disposable`, `is_undeliverable`); malformed input still returns `400`. Nothing is persisted. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + **Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price. + + ``POST /v3/email/check/`` + + Args: + email: The same email address used in the matching [`POST /v3/email/send/`](/standalone-apis/email-send) call. This is what links the check to the send. + code: The OTP the end user received: 4–8 digits, or 4–8 letters/digits when `alphanumeric_code: true` was used at send time. Comparison is case-insensitive. + duplicated_email_action: What to do when the same address was already used and approved by a different user (only previously Approved verifications count) (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings` and fills `email.matches`. + breached_email_action: What to do when the address appears in known data breaches (`email.is_breached`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`. + disposable_email_action: What to do when the domain belongs to a disposable/temporary-mail provider (`email.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`. + enable_social: When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price. + email_no_social_presence_action: Action when the social footprint check finds the address registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EmailVerificationCheckResponse", + self._client.request( + "post", + "/v3/email/check/", + cast_to=EmailVerificationCheckResponse, + auth="api_key", + body={ + "email": email, + "code": code, + "duplicated_email_action": duplicated_email_action, + "breached_email_action": breached_email_action, + "disposable_email_action": disposable_email_action, + "enable_social": enable_social, + "email_no_social_presence_action": email_no_social_presence_action, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncEmail(AsyncAPIResource): + """The ``email`` resource of the Didit API. + + Methods: ``assess_risk``, ``check_code``, ``send_code``. + """ + + @cached_property + def with_raw_response(self) -> AsyncEmailWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncEmailWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncEmailWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncEmailWithStreamingResponse(self) + + async def assess_risk( + self, + *, + email: str, + country_code: str | NotGiven = NOT_GIVEN, + signals: RiskSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EmailRiskResponse: + """Risk-score an email address. + + Return deliverability, disposable-address, breach, cross-session match, and normalized risk-intelligence signals without sending an OTP. The endpoint creates an API session and returns its id as `request_id`. + + **Decision.** The top-level `status` mirrors `email.status`. It is normally `Approved`; the risk evaluation can produce `In Review` or `Declined`. + + **Compatibility aliases.** Live responses expose the normalized intelligence object as both `email.email_intelligence` and `email.enrichment`. The values are identical. New integrations can read `email.enrichment`. Both keys are absent when no usable intelligence data was returned. + + **Billing.** Live calls use Email Risk API pricing. The request is billable once the lookup starts, even when some best-effort fields are unavailable. + + **Sandbox.** Sandbox keys validate the request and return deterministic approved data without sending an email, consuming credits, creating a session, or contributing to Networks. + + **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + + **Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price. + + ``POST /v3/email/risk/`` + + Args: + email: Email address to score. + country_code: Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage. + signals: Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks. + vendor_data: Your stable reference for this request. Stored on the API session and echoed in the response. + metadata: Your non-sensitive JSON metadata. Stored on the API session and echoed in the response. + enable_social: When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EmailRiskResponse", + await self._client.request( + "post", + "/v3/email/risk/", + cast_to=EmailRiskResponse, + auth="api_key", + body={ + "email": email, + "country_code": country_code, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + "enable_social": enable_social, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def send_code( + self, + *, + email: str, + options: EmailSendCodeParamsOptions | NotGiven = NOT_GIVEN, + signals: EmailSendCodeParamsSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EmailVerificationSendResponse: + """Send Email Code. + + Send a one-time passcode (OTP) to an email address, then verify it with [`POST /v3/email/check/`](/standalone-apis/email-check). + + **How send and check pair up.** Verification state is keyed by your application plus the `email` address - the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same address and the code the user received before the window closes. Calling send again for the same address while a verification is pending generates a fresh code for that same verification (`status: "Retry"`, same `request_id`); the previous code stops working. At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries. + + **Deliverability pre-check.** Before anything is sent, the address goes through syntax and DNS/MX validation. Addresses that cannot receive mail return `200` with `status: "Undeliverable"` and `reason: "email_can_not_be_delivered"` - no email is sent, nothing is billed, and the verification is immediately finalized as `Declined` (a follow-up check returns `Expired or Not Found`). A downstream send failure reports the same way. + + **Code format and branding.** Codes are 4–8 characters (`options.code_size`, default 6), numeric by default; set `options.alphanumeric_code: true` for uppercase letters and digits (the check comparison is case-insensitive). The email template is localized via `options.locale` (54 supported languages) and can use your application's white-label branding via `options.use_white_label_customization`. + + **Billing.** One Email Verification API credit per successful send (`status: "Success"`), charged at send time. `Retry` and `Undeliverable` sends are free, and checks are free. + + **Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses. + + **Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no email is sent and nothing is persisted. Use code `123456` on the sandbox check. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/email/send/`` + + Args: + email: Recipient email address. Malformed addresses return `400`; syntactically valid addresses that cannot receive mail (failed DNS/MX validation) return `200` with `status: "Undeliverable"`. + options: OTP format, localization, and branding options. All fields are optional. + signals: Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional. + vendor_data: Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record. + metadata: Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EmailVerificationSendResponse", + await self._client.request( + "post", + "/v3/email/send/", + cast_to=EmailVerificationSendResponse, + auth="api_key", + body={ + "email": email, + "options": options, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def check_code( + self, + *, + email: str, + code: str, + duplicated_email_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + breached_email_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + disposable_email_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + email_no_social_presence_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> EmailVerificationCheckResponse: + """Check Email Code. + + Verify the OTP delivered by [`POST /v3/email/send/`](/standalone-apis/email-send) and get the final verification result plus email intelligence: deliverability, breach exposure (`is_breached` and the `breaches` list from a breach-intelligence database), disposable-provider detection, and duplicate usage of the address across your sessions. + + **How the check finds the verification.** Matching is by your application plus the `email` address - `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, undeliverable at send time, or older than 5 minutes) the endpoint returns `200` with `status: "Expired or Not Found"`. + + **Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: "Failed"` with the attempts remaining and `email: null`; the third wrong code finalizes the verification as `Declined` with an `EMAIL_CODE_ATTEMPTS_EXCEEDED` warning and returns the full email report. Codes are compared case-insensitively (relevant for `alphanumeric_code` sends); only the most recently sent code is valid. + + **Outcomes.** `Approved` - correct code and no declining risk. `Declined` - terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted address, or the address found undeliverable at finalization), or the attempt budget was exhausted. `Failed` - wrong code, attempts remaining. `Expired or Not Found` - nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `email` carries the full report (`is_breached`, `breaches`, `is_disposable`, `is_undeliverable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID. + + **Risk actions.** `duplicated_email_action`, `breached_email_action`, and `disposable_email_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `email.warnings` without affecting the status. + + **Billing.** Checks are free - the credit is consumed by the successful send. + + **Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook. + + **Sandbox.** Sandbox API keys skip all processing: any well-formed code (`123456` included) returns a static `Approved` payload with a simplified `email` object (`status`, `email`, `is_breached`, `is_disposable`, `is_undeliverable`); malformed input still returns `400`. Nothing is persisted. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + **Social footprint.** Set `enable_social` to `true` to add a `email_social` block: the online platforms this address is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the address was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `email_social` unit per request on top of this endpoint's own price. + + ``POST /v3/email/check/`` + + Args: + email: The same email address used in the matching [`POST /v3/email/send/`](/standalone-apis/email-send) call. This is what links the check to the send. + code: The OTP the end user received: 4–8 digits, or 4–8 letters/digits when `alphanumeric_code: true` was used at send time. Comparison is case-insensitive. + duplicated_email_action: What to do when the same address was already used and approved by a different user (only previously Approved verifications count) (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings` and fills `email.matches`. + breached_email_action: What to do when the address appears in known data breaches (`email.is_breached`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`. + disposable_email_action: What to do when the domain belongs to a disposable/temporary-mail provider (`email.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`. + enable_social: When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price. + email_no_social_presence_action: Action when the social footprint check finds the address registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "EmailVerificationCheckResponse", + await self._client.request( + "post", + "/v3/email/check/", + cast_to=EmailVerificationCheckResponse, + auth="api_key", + body={ + "email": email, + "code": code, + "duplicated_email_action": duplicated_email_action, + "breached_email_action": breached_email_action, + "disposable_email_action": disposable_email_action, + "enable_social": enable_social, + "email_no_social_presence_action": email_no_social_presence_action, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class EmailWithRawResponse: + def __init__(self, resource: Email) -> None: + self._resource = resource + self.assess_risk = to_raw_response_wrapper(resource.assess_risk) + self.send_code = to_raw_response_wrapper(resource.send_code) + self.check_code = to_raw_response_wrapper(resource.check_code) + + +class AsyncEmailWithRawResponse: + def __init__(self, resource: AsyncEmail) -> None: + self._resource = resource + self.assess_risk = async_to_raw_response_wrapper(resource.assess_risk) + self.send_code = async_to_raw_response_wrapper(resource.send_code) + self.check_code = async_to_raw_response_wrapper(resource.check_code) + + +class EmailWithStreamingResponse: + def __init__(self, resource: Email) -> None: + self._resource = resource + self.assess_risk = to_streamed_response_wrapper(resource.assess_risk) + self.send_code = to_streamed_response_wrapper(resource.send_code) + self.check_code = to_streamed_response_wrapper(resource.check_code) + + +class AsyncEmailWithStreamingResponse: + def __init__(self, resource: AsyncEmail) -> None: + self._resource = resource + self.assess_risk = async_to_streamed_response_wrapper(resource.assess_risk) + self.send_code = async_to_streamed_response_wrapper(resource.send_code) + self.check_code = async_to_streamed_response_wrapper(resource.check_code) diff --git a/src/didit/resources/face_match.py b/src/didit/resources/face_match.py new file mode 100644 index 0000000..8daa08b --- /dev/null +++ b/src/didit/resources/face_match.py @@ -0,0 +1,238 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``face_match`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.face_match.compare_response import FaceMatchCompareResponse + +__all__ = [ + "FaceMatch", + "AsyncFaceMatch", + "FaceMatchWithRawResponse", + "AsyncFaceMatchWithRawResponse", + "FaceMatchWithStreamingResponse", + "AsyncFaceMatchWithStreamingResponse", +] + + +class FaceMatch(SyncAPIResource): + """The ``face_match`` resource of the Didit API. + + Methods: ``compare``. + """ + + @cached_property + def with_raw_response(self) -> FaceMatchWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return FaceMatchWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FaceMatchWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return FaceMatchWithStreamingResponse(self) + + def compare( + self, + *, + user_image: FileTypes, + ref_image: FileTypes, + face_match_score_decline_threshold: float | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FaceMatchCompareResponse: + """Face Match. + + Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`. + + **How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: "Approved"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: "Declined"` and a `NO_REFERENCE_IMAGE` warning - `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results. + + **Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. + + **Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening - use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/face-match/`` + + Args: + user_image: Live or candidate face image to verify (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. + ref_image: Reference face image to compare against (e.g. an ID document portrait or a previously enrolled photo). Same format and size limits as `user_image`. + face_match_score_decline_threshold: Similarity threshold (0–100) that drives the response `status`. A computed score **at or below** this value sets `status` to `Declined` and adds a `LOW_FACE_MATCH_SIMILARITY` warning; a score strictly above it returns `Approved`. Default `30` is permissive - raise it (e.g. `50`–`60`) for stricter verification. Values outside 0–100 return `400`. + rotate_image: When `true`, the service tries the input images in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection - useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images. + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, and announced through a `status.updated` webhook. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"login"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FaceMatchCompareResponse", + self._client.request( + "post", + "/v3/face-match/", + cast_to=FaceMatchCompareResponse, + auth="api_key", + body={ + "user_image": user_image, + "ref_image": ref_image, + "face_match_score_decline_threshold": face_match_score_decline_threshold, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=( + "ref_image", + "user_image", + ), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncFaceMatch(AsyncAPIResource): + """The ``face_match`` resource of the Didit API. + + Methods: ``compare``. + """ + + @cached_property + def with_raw_response(self) -> AsyncFaceMatchWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncFaceMatchWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFaceMatchWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncFaceMatchWithStreamingResponse(self) + + async def compare( + self, + *, + user_image: FileTypes, + ref_image: FileTypes, + face_match_score_decline_threshold: float | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FaceMatchCompareResponse: + """Face Match. + + Compare two face images (1:1) and get back a similarity `score` (0–100) plus an `Approved`/`Declined` `status`. + + **How it works.** Both images are decoded and analyzed by a biometric model (set `rotate_image=true` if captures may be sideways or upside down), then a similarity score between the two faces is computed. A score **strictly above** `face_match_score_decline_threshold` (default `30`) returns `status: "Approved"`; a score at or below it returns `Declined` with a `LOW_FACE_MATCH_SIMILARITY` warning. If no face pair can be scored at all, the call still returns `200` with `status: "Declined"` and a `NO_REFERENCE_IMAGE` warning - `score` is `null` on the default save path, or `0` with `save_api_request=false`. Submit one clear, front-facing face per image for best results. + + **Billing.** Each `200` response consumes one Face Match API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, both images are stored as the session's face-match source/target images, and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. + + **Face search index.** This endpoint never enrolls faces into your face search index and never runs blocklist or duplicate screening - use `POST /v3/passive-liveness/` or `POST /v3/face-search/` for those. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/face-match/`` + + Args: + user_image: Live or candidate face image to verify (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. + ref_image: Reference face image to compare against (e.g. an ID document portrait or a previously enrolled photo). Same format and size limits as `user_image`. + face_match_score_decline_threshold: Similarity threshold (0–100) that drives the response `status`. A computed score **at or below** this value sets `status` to `Declined` and adds a `LOW_FACE_MATCH_SIMILARITY` warning; a score strictly above it returns `Approved`. Default `30` is permissive - raise it (e.g. `50`–`60`) for stricter verification. Values outside 0–100 return `400`. + rotate_image: When `true`, the service tries the input images in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection - useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images. + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, and announced through a `status.updated` webhook. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"login"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FaceMatchCompareResponse", + await self._client.request( + "post", + "/v3/face-match/", + cast_to=FaceMatchCompareResponse, + auth="api_key", + body={ + "user_image": user_image, + "ref_image": ref_image, + "face_match_score_decline_threshold": face_match_score_decline_threshold, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=( + "ref_image", + "user_image", + ), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class FaceMatchWithRawResponse: + def __init__(self, resource: FaceMatch) -> None: + self._resource = resource + self.compare = to_raw_response_wrapper(resource.compare) + + +class AsyncFaceMatchWithRawResponse: + def __init__(self, resource: AsyncFaceMatch) -> None: + self._resource = resource + self.compare = async_to_raw_response_wrapper(resource.compare) + + +class FaceMatchWithStreamingResponse: + def __init__(self, resource: FaceMatch) -> None: + self._resource = resource + self.compare = to_streamed_response_wrapper(resource.compare) + + +class AsyncFaceMatchWithStreamingResponse: + def __init__(self, resource: AsyncFaceMatch) -> None: + self._resource = resource + self.compare = async_to_streamed_response_wrapper(resource.compare) diff --git a/src/didit/resources/face_search.py b/src/didit/resources/face_search.py new file mode 100644 index 0000000..f821878 --- /dev/null +++ b/src/didit/resources/face_search.py @@ -0,0 +1,236 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``face_search`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.face_search.search_response import FaceSearchSearchResponse + +__all__ = [ + "FaceSearch", + "AsyncFaceSearch", + "FaceSearchWithRawResponse", + "AsyncFaceSearchWithRawResponse", + "FaceSearchWithStreamingResponse", + "AsyncFaceSearchWithStreamingResponse", +] + + +class FaceSearch(SyncAPIResource): + """The ``face_search`` resource of the Didit API. + + Methods: ``search``. + """ + + @cached_property + def with_raw_response(self) -> FaceSearchWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return FaceSearchWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FaceSearchWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return FaceSearchWithStreamingResponse(self) + + def search( + self, + *, + user_image: FileTypes, + search_type: Literal["most_similar", "blocklisted_or_approved"] + | str + | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FaceSearchSearchResponse: + """Face Search. + + Search a face image (1:N) against your application's face search index - faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists - and get back ranked similarity matches plus blocklist and duplicate warnings. + + **How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session's document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`). + + - `search_type=most_similar` (default) ranks every enrolled face by similarity - use it for deduplication and fraud-ring investigation. + - `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first - use it when the primary goal is blocklist screening. + + `status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response. + + **Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. + + **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/face-search/`` + + Args: + user_image: Front-facing face image to search with. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image must contain at least one detectable face - otherwise the endpoint returns `400`. When several faces are present, the largest one is searched and a `MULTIPLE_FACES_DETECTED` warning is added. + search_type: Search policy. `most_similar` (default) ranks every enrolled face in your application by similarity. `blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces - with blocklisted entries ranked first. + rotate_image: When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency. + save_api_request: When `true` (default), persists the call as an API-type session, emits a `status.updated` webhook, presigns `match_image_url` values, and enrolls the searched face into your face search index (Face Search enrollments are excluded from future searches). When `false`, the search is one-shot - no session is stored and the face is not enrolled. + vendor_data: Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. It does not exclude same-user faces from the search results. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FaceSearchSearchResponse", + self._client.request( + "post", + "/v3/face-search/", + cast_to=FaceSearchSearchResponse, + auth="api_key", + body={ + "user_image": user_image, + "search_type": search_type, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("user_image",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncFaceSearch(AsyncAPIResource): + """The ``face_search`` resource of the Didit API. + + Methods: ``search``. + """ + + @cached_property + def with_raw_response(self) -> AsyncFaceSearchWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncFaceSearchWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFaceSearchWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncFaceSearchWithStreamingResponse(self) + + async def search( + self, + *, + user_image: FileTypes, + search_type: Literal["most_similar", "blocklisted_or_approved"] + | str + | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FaceSearchSearchResponse: + """Face Search. + + Search a face image (1:N) against your application's face search index - faces enrolled from verification sessions, saved standalone API calls, direct user-profile face uploads, and your block/allow lists - and get back ranked similarity matches plus blocklist and duplicate warnings. + + **How it works.** The largest detected face in `user_image` is embedded and searched against the index, scoped to your application. Up to **5 matches** above the similarity floor are returned in `matches`, each with `similarity_percentage`, the originating session (`session_id`, `session_number`, `status`, `vendor_data`), `user_details` extracted during that session's document verification, `is_blocklisted` / `is_allowlisted` flags, and a `source` discriminator (`session`, `imported`, or `list_entry`). + + - `search_type=most_similar` (default) ranks every enrolled face by similarity - use it for deduplication and fraud-ring investigation. + - `search_type=blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces, ranking blocklisted entries first - use it when the primary goal is blocklist screening. + + `status` is `Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fires; `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) and `MULTIPLE_FACES_DETECTED` (`warning`) never decline. Unlike Passive Liveness, this endpoint does **not** exclude prior sessions with the same `vendor_data` from matching. A passive-liveness analysis also runs internally and is stored with the persisted session, but its result is not part of this response. + + **Billing.** Each `200` response consumes one Face Search API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. + + **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook) and the searched face is enrolled into your face search index. Faces enrolled by Face Search calls are excluded from future Face Search results, so repeated searches never match each other. When `false`, the call is one-shot: nothing is stored, the face is not enrolled, and `match_image_url` values are internal storage paths rather than downloadable URLs. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/face-search/`` + + Args: + user_image: Front-facing face image to search with. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image must contain at least one detectable face - otherwise the endpoint returns `400`. When several faces are present, the largest one is searched and a `MULTIPLE_FACES_DETECTED` warning is added. + search_type: Search policy. `most_similar` (default) ranks every enrolled face in your application by similarity. `blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces - with blocklisted entries ranked first. + rotate_image: When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency. + save_api_request: When `true` (default), persists the call as an API-type session, emits a `status.updated` webhook, presigns `match_image_url` values, and enrolls the searched face into your face search index (Face Search enrollments are excluded from future searches). When `false`, the search is one-shot - no session is stored and the face is not enrolled. + vendor_data: Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. It does not exclude same-user faces from the search results. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FaceSearchSearchResponse", + await self._client.request( + "post", + "/v3/face-search/", + cast_to=FaceSearchSearchResponse, + auth="api_key", + body={ + "user_image": user_image, + "search_type": search_type, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("user_image",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class FaceSearchWithRawResponse: + def __init__(self, resource: FaceSearch) -> None: + self._resource = resource + self.search = to_raw_response_wrapper(resource.search) + + +class AsyncFaceSearchWithRawResponse: + def __init__(self, resource: AsyncFaceSearch) -> None: + self._resource = resource + self.search = async_to_raw_response_wrapper(resource.search) + + +class FaceSearchWithStreamingResponse: + def __init__(self, resource: FaceSearch) -> None: + self._resource = resource + self.search = to_streamed_response_wrapper(resource.search) + + +class AsyncFaceSearchWithStreamingResponse: + def __init__(self, resource: AsyncFaceSearch) -> None: + self._resource = resource + self.search = async_to_streamed_response_wrapper(resource.search) diff --git a/src/didit/resources/fraud_checks.py b/src/didit/resources/fraud_checks.py new file mode 100644 index 0000000..960d8e3 --- /dev/null +++ b/src/didit/resources/fraud_checks.py @@ -0,0 +1,418 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``fraud_checks`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.fraud_check_context_request import FraudCheckContextRequest +from didit.types.components.fraud_check_module_selection_request import ( + FraudCheckModuleSelectionRequest, +) +from didit.types.components.fraud_check_plan import FraudCheckPlan +from didit.types.components.fraud_check_response import FraudCheckResponse +from didit.types.components.fraud_check_subject_request import FraudCheckSubjectRequest + +__all__ = [ + "FraudChecks", + "AsyncFraudChecks", + "FraudChecksWithRawResponse", + "AsyncFraudChecksWithRawResponse", + "FraudChecksWithStreamingResponse", + "AsyncFraudChecksWithStreamingResponse", +] + + +class FraudChecks(SyncAPIResource): + """The ``fraud_checks`` resource of the Didit API. + + Methods: ``create``, ``plan``, ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> FraudChecksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return FraudChecksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> FraudChecksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return FraudChecksWithStreamingResponse(self) + + def retrieve( + self, + check_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FraudCheckResponse: + """Retrieve a Fraud Check. + + Fetch a previously created check by its `check_id`. + + ``GET /v3/fraud/checks/{check_id}/`` + + Args: + check_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FraudCheckResponse", + self._client.request( + "get", + "/v3/fraud/checks/{check_id}/", + cast_to=FraudCheckResponse, + path_params={"check_id": check_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + subject: FraudCheckSubjectRequest, + client_reference: str | NotGiven = NOT_GIVEN, + profile: str | NotGiven = NOT_GIVEN, + profile_version: str | NotGiven = NOT_GIVEN, + context: FraudCheckContextRequest | NotGiven = NOT_GIVEN, + checks: FraudCheckModuleSelectionRequest | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FraudCheckResponse: + """Run a Fraud Check. + + Submit one or more typed identifiers and receive normalization, fresh enrichment, authoritative validation where the country and identifier type are covered, independent risk dimensions and a recommendation in one response. + + These profiles support identifier checks. Selfie-based identity binding (`biometric_identity`) requires profile version `2026-09-14.2` and a Brazilian CPF; it runs only with the user's consent and when your organization is entitled to it. Earlier versions report it as `not_available_in_profile_version`. Modules that cannot run are reported as skipped with a deterministic reason; a source that could not answer is never reported as an identity mismatch. + + ``POST /v3/fraud/checks/`` + + Args: + subject + client_reference: Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409. + profile: Orchestration profile id. Defaults to the Didit `standard` profile. + profile_version: Pin a frozen profile version. Defaults to the current version of the profile. + context + checks + vendor_data + metadata + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FraudCheckResponse", + self._client.request( + "post", + "/v3/fraud/checks/", + cast_to=FraudCheckResponse, + auth="api_key", + body={ + "subject": subject, + "client_reference": client_reference, + "profile": profile, + "profile_version": profile_version, + "context": context, + "checks": checks, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def plan( + self, + *, + subject: FraudCheckSubjectRequest, + client_reference: str | NotGiven = NOT_GIVEN, + profile: str | NotGiven = NOT_GIVEN, + profile_version: str | NotGiven = NOT_GIVEN, + context: FraudCheckContextRequest | NotGiven = NOT_GIVEN, + checks: FraudCheckModuleSelectionRequest | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FraudCheckPlan: + """Plan a Fraud Check. + + Validate selected checks and estimate their price without executing, creating a session or billing. + + ``POST /v3/fraud/checks/plan/`` + + Args: + subject + client_reference: Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409. + profile: Orchestration profile id. Defaults to the Didit `standard` profile. + profile_version: Pin a frozen profile version. Defaults to the current version of the profile. + context + checks + vendor_data + metadata + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FraudCheckPlan", + self._client.request( + "post", + "/v3/fraud/checks/plan/", + cast_to=FraudCheckPlan, + auth="api_key", + body={ + "subject": subject, + "client_reference": client_reference, + "profile": profile, + "profile_version": profile_version, + "context": context, + "checks": checks, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncFraudChecks(AsyncAPIResource): + """The ``fraud_checks`` resource of the Didit API. + + Methods: ``create``, ``plan``, ``retrieve``. + """ + + @cached_property + def with_raw_response(self) -> AsyncFraudChecksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncFraudChecksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncFraudChecksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncFraudChecksWithStreamingResponse(self) + + async def retrieve( + self, + check_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FraudCheckResponse: + """Retrieve a Fraud Check. + + Fetch a previously created check by its `check_id`. + + ``GET /v3/fraud/checks/{check_id}/`` + + Args: + check_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FraudCheckResponse", + await self._client.request( + "get", + "/v3/fraud/checks/{check_id}/", + cast_to=FraudCheckResponse, + path_params={"check_id": check_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + subject: FraudCheckSubjectRequest, + client_reference: str | NotGiven = NOT_GIVEN, + profile: str | NotGiven = NOT_GIVEN, + profile_version: str | NotGiven = NOT_GIVEN, + context: FraudCheckContextRequest | NotGiven = NOT_GIVEN, + checks: FraudCheckModuleSelectionRequest | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FraudCheckResponse: + """Run a Fraud Check. + + Submit one or more typed identifiers and receive normalization, fresh enrichment, authoritative validation where the country and identifier type are covered, independent risk dimensions and a recommendation in one response. + + These profiles support identifier checks. Selfie-based identity binding (`biometric_identity`) requires profile version `2026-09-14.2` and a Brazilian CPF; it runs only with the user's consent and when your organization is entitled to it. Earlier versions report it as `not_available_in_profile_version`. Modules that cannot run are reported as skipped with a deterministic reason; a source that could not answer is never reported as an identity mismatch. + + ``POST /v3/fraud/checks/`` + + Args: + subject + client_reference: Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409. + profile: Orchestration profile id. Defaults to the Didit `standard` profile. + profile_version: Pin a frozen profile version. Defaults to the current version of the profile. + context + checks + vendor_data + metadata + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FraudCheckResponse", + await self._client.request( + "post", + "/v3/fraud/checks/", + cast_to=FraudCheckResponse, + auth="api_key", + body={ + "subject": subject, + "client_reference": client_reference, + "profile": profile, + "profile_version": profile_version, + "context": context, + "checks": checks, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def plan( + self, + *, + subject: FraudCheckSubjectRequest, + client_reference: str | NotGiven = NOT_GIVEN, + profile: str | NotGiven = NOT_GIVEN, + profile_version: str | NotGiven = NOT_GIVEN, + context: FraudCheckContextRequest | NotGiven = NOT_GIVEN, + checks: FraudCheckModuleSelectionRequest | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> FraudCheckPlan: + """Plan a Fraud Check. + + Validate selected checks and estimate their price without executing, creating a session or billing. + + ``POST /v3/fraud/checks/plan/`` + + Args: + subject + client_reference: Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409. + profile: Orchestration profile id. Defaults to the Didit `standard` profile. + profile_version: Pin a frozen profile version. Defaults to the current version of the profile. + context + checks + vendor_data + metadata + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "FraudCheckPlan", + await self._client.request( + "post", + "/v3/fraud/checks/plan/", + cast_to=FraudCheckPlan, + auth="api_key", + body={ + "subject": subject, + "client_reference": client_reference, + "profile": profile, + "profile_version": profile_version, + "context": context, + "checks": checks, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class FraudChecksWithRawResponse: + def __init__(self, resource: FraudChecks) -> None: + self._resource = resource + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.create = to_raw_response_wrapper(resource.create) + self.plan = to_raw_response_wrapper(resource.plan) + + +class AsyncFraudChecksWithRawResponse: + def __init__(self, resource: AsyncFraudChecks) -> None: + self._resource = resource + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.create = async_to_raw_response_wrapper(resource.create) + self.plan = async_to_raw_response_wrapper(resource.plan) + + +class FraudChecksWithStreamingResponse: + def __init__(self, resource: FraudChecks) -> None: + self._resource = resource + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.create = to_streamed_response_wrapper(resource.create) + self.plan = to_streamed_response_wrapper(resource.plan) + + +class AsyncFraudChecksWithStreamingResponse: + def __init__(self, resource: AsyncFraudChecks) -> None: + self._resource = resource + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.create = async_to_streamed_response_wrapper(resource.create) + self.plan = async_to_streamed_response_wrapper(resource.plan) diff --git a/src/didit/resources/id_verification.py b/src/didit/resources/id_verification.py new file mode 100644 index 0000000..350513f --- /dev/null +++ b/src/didit/resources/id_verification.py @@ -0,0 +1,298 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``id_verification`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.id_verification.verify_response import IdVerificationVerifyResponse + +__all__ = [ + "IdVerification", + "AsyncIdVerification", + "IdVerificationWithRawResponse", + "AsyncIdVerificationWithRawResponse", + "IdVerificationWithStreamingResponse", + "AsyncIdVerificationWithStreamingResponse", +] + + +class IdVerification(SyncAPIResource): + """The ``id_verification`` resource of the Didit API. + + Methods: ``verify``. + """ + + @cached_property + def with_raw_response(self) -> IdVerificationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return IdVerificationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> IdVerificationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return IdVerificationWithStreamingResponse(self) + + def verify( + self, + *, + front_image: FileTypes, + front_image_password: str | NotGiven = NOT_GIVEN, + back_image: FileTypes | NotGiven = NOT_GIVEN, + back_image_password: str | NotGiven = NOT_GIVEN, + perform_document_liveness: bool | NotGiven = NOT_GIVEN, + minimum_age: int | None | NotGiven = NOT_GIVEN, + expiration_date_not_detected_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + invalid_mrz_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + inconsistent_data_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + critical_field_occlusion_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + document_or_personal_number_format_mismatch_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + preferred_characters: Literal["latin", "non_latin"] | str | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IdVerificationVerifyResponse: + """ID Verification (document OCR + fraud checks). + + OCR an identity document and run document fraud checks in one call - returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories. + + **How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible - the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation. + + **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{"error": "COULD_NOT_RECOGNIZE_DOCUMENT"}`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code. + + **Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional "Sandbox User" USA identity card), no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/id-verification/`` + + Args: + front_image: Front side of the identity document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`, `pdf`. Maximum upload size: **10 MB** (larger files are rejected with `400`). PDFs are rendered to an image before OCR; encrypted PDFs require `front_image_password`. Images are automatically compressed to ~0.5 MB and EXIF orientation is applied. Upload the full, uncropped capture with all four corners of the document visible. + front_image_password: Password to decrypt `front_image` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`. + back_image: Back side of the document - send it whenever the document has one (many ID cards carry the MRZ or a barcode on the back; omitting it simply leaves the MRZ and back-side fields empty in the response - this endpoint does not raise a missing-MRZ warning). Same format and size limits as `front_image`. When omitted, `back_image` and back-side fields are `null` in the response. + back_image_password: Password to decrypt `back_image` when it is an encrypted PDF. + perform_document_liveness: Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency. + minimum_age: Accepted and validated (1–120) but **currently not applied** by this endpoint - it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions. + expiration_date_not_detected_action: What to do when no expiration date can be read (`EXPIRATION_DATE_NOT_DETECTED`). Default `NO_ACTION` because many identity documents print no expiry date. Note: a *detected and past* expiry date always declines (`DOCUMENT_EXPIRED`), regardless of this option. + invalid_mrz_action: What to do when the extracted MRZ fails check-digit validation (`MRZ_VALIDATION_FAILED`). A missing MRZ raises no warning on this endpoint. Only relevant for documents that carry an MRZ. `REVIEW` is not supported on this endpoint and returns `400`. + inconsistent_data_action: What to do when extracted data is internally inconsistent: `DATA_INCONSISTENT`, `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` (visual zone disagrees with the MRZ), or `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS`. + critical_field_occlusion_action: What to do when a critical printed field was physically covered on the capture (`CRITICAL_FIELD_OCCLUSION_DETECTED`) - an identity number, a name, the date of birth, or the expiry date. Declines by default, like this endpoint's other document-integrity risks: covering the numbers a report is reconciled against is what a redacted document looks like. The value read for a covered field is dropped from the extraction either way, so the matching `*_NOT_DETECTED` warning usually accompanies it. `REVIEW` is not supported on this endpoint and returns `400`. + document_or_personal_number_format_mismatch_action: What to do when a number does not match the format its issuing authority uses for that document type (`DOCUMENT_NUMBER_FORMAT_MISMATCH`, `PERSONAL_NUMBER_FORMAT_MISMATCH`). Default `NO_ACTION`, which reports the warning without changing the status. `REVIEW` is not supported on this endpoint and returns `400`. + preferred_characters: Preferred script for name/address fields on documents that print both Latin and non-Latin text (Arabic, Cyrillic, CJK, …). `latin` returns transliterated/Latin values; `non_latin` prefers the native script. + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with document images returned as media URLs. When `false`, nothing is stored, `request_id` is a transient UUID, and images are returned inline as base64 JPEG strings. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "IdVerificationVerifyResponse", + self._client.request( + "post", + "/v3/id-verification/", + cast_to=IdVerificationVerifyResponse, + auth="api_key", + body={ + "front_image": front_image, + "front_image_password": front_image_password, + "back_image": back_image, + "back_image_password": back_image_password, + "perform_document_liveness": perform_document_liveness, + "minimum_age": minimum_age, + "expiration_date_not_detected_action": expiration_date_not_detected_action, + "invalid_mrz_action": invalid_mrz_action, + "inconsistent_data_action": inconsistent_data_action, + "critical_field_occlusion_action": critical_field_occlusion_action, + "document_or_personal_number_format_mismatch_action": document_or_personal_number_format_mismatch_action, + "preferred_characters": preferred_characters, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=( + "back_image", + "front_image", + ), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncIdVerification(AsyncAPIResource): + """The ``id_verification`` resource of the Didit API. + + Methods: ``verify``. + """ + + @cached_property + def with_raw_response(self) -> AsyncIdVerificationWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncIdVerificationWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncIdVerificationWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncIdVerificationWithStreamingResponse(self) + + async def verify( + self, + *, + front_image: FileTypes, + front_image_password: str | NotGiven = NOT_GIVEN, + back_image: FileTypes | NotGiven = NOT_GIVEN, + back_image_password: str | NotGiven = NOT_GIVEN, + perform_document_liveness: bool | NotGiven = NOT_GIVEN, + minimum_age: int | None | NotGiven = NOT_GIVEN, + expiration_date_not_detected_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + invalid_mrz_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + inconsistent_data_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + critical_field_occlusion_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + document_or_personal_number_format_mismatch_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + preferred_characters: Literal["latin", "non_latin"] | str | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IdVerificationVerifyResponse: + """ID Verification (document OCR + fraud checks). + + OCR an identity document and run document fraud checks in one call - returns the extracted holder fields, the parsed MRZ, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. Supports 14,000+ document types from 220+ countries and territories. + + **How it works.** Send the document's `front_image` (required) and, for two-sided documents, the `back_image`. Upload uncropped photos with all four corners of the document visible - the service detects, aligns, and crops the document itself, so do not pre-crop. The document is classified automatically (you never declare the country or document type), the visual zone is read with OCR, and the MRZ and any barcodes are decoded. Cross-checks then validate dates, number formats, and MRZ check digits, and compare the visual zone against the MRZ. With `perform_document_liveness=true`, the images are additionally screened for screen replays, printed copies, and portrait manipulation. + + **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Fraud and hard-failure risks always decline: `DOCUMENT_EXPIRED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, plus extraction failures (`NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`, `DOCUMENT_NUMBER_NOT_DETECTED`). (`PORTRAIT_IMAGE_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `INVALID_DATE`, and `MRZ_NOT_DETECTED` exist in workflow sessions but are never produced by this standalone endpoint.) Three risk groups are configurable per request via `invalid_mrz_action`, `inconsistent_data_action`, and `expiration_date_not_detected_action` (`DECLINE` or `NO_ACTION`). All other warnings (e.g. `POSSIBLE_DUPLICATED_USER`) are informational and never decline on their own. A document that cannot be processed at all returns `400` with `{"error": "COULD_NOT_RECOGNIZE_DOCUMENT"}`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `id_verification.status` and `id_verification.warnings`, not just the HTTP code. + + **Billing.** Each `200` response consumes one ID Verification API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any image processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the cropped document/portrait images are stored and returned as short-lived media URLs (`https:///ocr/...`), and a `status.updated` webhook is emitted to your configured webhook endpoints. When `false`, nothing is stored, `request_id` is a one-off correlation UUID, and `portrait_image`/`front_image`/`back_image` are returned inline as base64-encoded JPEG strings. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional "Sandbox User" USA identity card), no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/id-verification/`` + + Args: + front_image: Front side of the identity document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`, `pdf`. Maximum upload size: **10 MB** (larger files are rejected with `400`). PDFs are rendered to an image before OCR; encrypted PDFs require `front_image_password`. Images are automatically compressed to ~0.5 MB and EXIF orientation is applied. Upload the full, uncropped capture with all four corners of the document visible. + front_image_password: Password to decrypt `front_image` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`. + back_image: Back side of the document - send it whenever the document has one (many ID cards carry the MRZ or a barcode on the back; omitting it simply leaves the MRZ and back-side fields empty in the response - this endpoint does not raise a missing-MRZ warning). Same format and size limits as `front_image`. When omitted, `back_image` and back-side fields are `null` in the response. + back_image_password: Password to decrypt `back_image` when it is an encrypted PDF. + perform_document_liveness: Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency. + minimum_age: Accepted and validated (1–120) but **currently not applied** by this endpoint - it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions. + expiration_date_not_detected_action: What to do when no expiration date can be read (`EXPIRATION_DATE_NOT_DETECTED`). Default `NO_ACTION` because many identity documents print no expiry date. Note: a *detected and past* expiry date always declines (`DOCUMENT_EXPIRED`), regardless of this option. + invalid_mrz_action: What to do when the extracted MRZ fails check-digit validation (`MRZ_VALIDATION_FAILED`). A missing MRZ raises no warning on this endpoint. Only relevant for documents that carry an MRZ. `REVIEW` is not supported on this endpoint and returns `400`. + inconsistent_data_action: What to do when extracted data is internally inconsistent: `DATA_INCONSISTENT`, `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` (visual zone disagrees with the MRZ), or `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS`. + critical_field_occlusion_action: What to do when a critical printed field was physically covered on the capture (`CRITICAL_FIELD_OCCLUSION_DETECTED`) - an identity number, a name, the date of birth, or the expiry date. Declines by default, like this endpoint's other document-integrity risks: covering the numbers a report is reconciled against is what a redacted document looks like. The value read for a covered field is dropped from the extraction either way, so the matching `*_NOT_DETECTED` warning usually accompanies it. `REVIEW` is not supported on this endpoint and returns `400`. + document_or_personal_number_format_mismatch_action: What to do when a number does not match the format its issuing authority uses for that document type (`DOCUMENT_NUMBER_FORMAT_MISMATCH`, `PERSONAL_NUMBER_FORMAT_MISMATCH`). Default `NO_ACTION`, which reports the warning without changing the status. `REVIEW` is not supported on this endpoint and returns `400`. + preferred_characters: Preferred script for name/address fields on documents that print both Latin and non-Latin text (Arabic, Cyrillic, CJK, …). `latin` returns transliterated/Latin values; `non_latin` prefers the native script. + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with document images returned as media URLs. When `false`, nothing is stored, `request_id` is a transient UUID, and images are returned inline as base64 JPEG strings. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "IdVerificationVerifyResponse", + await self._client.request( + "post", + "/v3/id-verification/", + cast_to=IdVerificationVerifyResponse, + auth="api_key", + body={ + "front_image": front_image, + "front_image_password": front_image_password, + "back_image": back_image, + "back_image_password": back_image_password, + "perform_document_liveness": perform_document_liveness, + "minimum_age": minimum_age, + "expiration_date_not_detected_action": expiration_date_not_detected_action, + "invalid_mrz_action": invalid_mrz_action, + "inconsistent_data_action": inconsistent_data_action, + "critical_field_occlusion_action": critical_field_occlusion_action, + "document_or_personal_number_format_mismatch_action": document_or_personal_number_format_mismatch_action, + "preferred_characters": preferred_characters, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=( + "back_image", + "front_image", + ), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class IdVerificationWithRawResponse: + def __init__(self, resource: IdVerification) -> None: + self._resource = resource + self.verify = to_raw_response_wrapper(resource.verify) + + +class AsyncIdVerificationWithRawResponse: + def __init__(self, resource: AsyncIdVerification) -> None: + self._resource = resource + self.verify = async_to_raw_response_wrapper(resource.verify) + + +class IdVerificationWithStreamingResponse: + def __init__(self, resource: IdVerification) -> None: + self._resource = resource + self.verify = to_streamed_response_wrapper(resource.verify) + + +class AsyncIdVerificationWithStreamingResponse: + def __init__(self, resource: AsyncIdVerification) -> None: + self._resource = resource + self.verify = async_to_streamed_response_wrapper(resource.verify) diff --git a/src/didit/resources/id_wallets.py b/src/didit/resources/id_wallets.py new file mode 100644 index 0000000..50495bd --- /dev/null +++ b/src/didit/resources/id_wallets.py @@ -0,0 +1,3422 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``id_wallets`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.digital_id_wallet_api_audkenni_wallet_verification_start_country_enum import ( + DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_connectid_wallet_verification_start_country_enum import ( + DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_country520_enum import ( + DigitalIdWalletApiCountry520Enum, +) +from didit.types.components.digital_id_wallet_api_country_afc_enum import ( + DigitalIdWalletApiCountryAfcEnum, +) +from didit.types.components.digital_id_wallet_api_country_e2d_enum import ( + DigitalIdWalletApiCountryE2dEnum, +) +from didit.types.components.digital_id_wallet_api_country_ee9_enum import ( + DigitalIdWalletApiCountryEe9Enum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_diia_wallet_verification_start_country_enum import ( + DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum import ( + DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_eudi_wallet_verification_start_country_enum import ( + DigitalIdWalletApiEudiWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum import ( + DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_ftn_wallet_verification_start_country_enum import ( + DigitalIdWalletApiFtnWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_govbr_wallet_verification_start_country_enum import ( + DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum import ( + DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_idin_wallet_verification_start_country_enum import ( + DigitalIdWalletApiIdinWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_itsme_wallet_verification_start_country_enum import ( + DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_mitid_wallet_verification_start_country_enum import ( + DigitalIdWalletApiMitidWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum import ( + DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum import ( + DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_smart_id_wallet_verification_start_country_enum import ( + DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum import ( + DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_wallet_catalog_response import ( + DigitalIdWalletApiWalletCatalogResponse, +) +from didit.types.components.digital_id_wallet_api_wallet_verification_response import ( + DigitalIdWalletApiWalletVerificationResponse, +) + +__all__ = [ + "IdWallets", + "AsyncIdWallets", + "IdWalletsWithRawResponse", + "AsyncIdWalletsWithRawResponse", + "IdWalletsWithStreamingResponse", + "AsyncIdWalletsWithStreamingResponse", +] + + +class IdWallets(SyncAPIResource): + """The ``id_wallets`` resource of the Didit API. + + Methods: ``cancel_verification``, ``list``, ``poll_verification``, ``retrieve_verification``, ``start_audkenni``, ``start_bankid_cz``, ``start_bankid_no``, ``start_bankid_se``, ``start_buypass``, ``start_connectid``, ``start_diia``, ``start_eparaksts_mobile``, ``start_eudi``, ``start_franceconnect``, ``start_frejaid``, ``start_ftn``, ``start_govbr``, ``start_govuk_wallet``, ``start_idcard_ee``, ``start_idin``, ``start_itsme``, ``start_mitid``, ``start_mobile_id``, ``start_mojeid``, ``start_oneid``, ``start_personalausweis``, ``start_smart_id``, ``start_uae_pass``, ``start_vipps``. + """ + + @cached_property + def with_raw_response(self) -> IdWalletsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return IdWalletsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> IdWalletsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return IdWalletsWithStreamingResponse(self) + + def list( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletCatalogResponse: + """List Digital ID wallets. + + Every Digital ID wallet with its start endpoint, countries, required fields and whether this application can start it in this environment now. Listed does not mean live. + + ``GET /v3/id-verification/wallets/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletCatalogResponse", + self._client.request( + "get", + "/v3/id-verification/wallets/", + cast_to=DigitalIdWalletApiWalletCatalogResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_verification( + self, + request_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Get a wallet verification. + + Status and result of a wallet verification: the normalized identity and the wallet's own attributes once verified, the error code once declined, the next action while pending. A verification past expires_at is decided timeout. + + ``GET /v3/id-verification/wallets/verifications/{request_id}/`` + + Args: + request_id: The request_id the start endpoint returned. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "get", + "/v3/id-verification/wallets/verifications/{request_id}/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + path_params={"request_id": request_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def poll_verification( + self, + request_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Poll a wallet verification. + + Smart-ID and Mobile-ID: ask the provider whether the user confirmed, refused or let the prompt lapse, and verify the signed identity when they confirmed. Redirect wallets complete through the user's browser, so for them this only reads the verification back. Answers the verification as it is afterwards. + + ``POST /v3/id-verification/wallets/verifications/{request_id}/poll/`` + + Args: + request_id: The request_id the start endpoint returned. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/verifications/{request_id}/poll/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + path_params={"request_id": request_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def cancel_verification( + self, + request_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Cancel a wallet verification. + + Withdraw a pending verification: it is declined as cancelled and never billed. A decided one is returned unchanged. Smart-ID and Mobile-ID: a cancel that arrives while a poll is checking the sign-in waits a few seconds for that poll, then declines it, or returns it verified if the user had already confirmed. A verification the user is completing right now cannot be cancelled (409 verification_in_progress with Retry-After): retry after that many seconds to read its outcome. + + ``POST /v3/id-verification/wallets/verifications/{request_id}/cancel/`` + + Args: + request_id: The request_id the start endpoint returned. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/verifications/{request_id}/cancel/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + path_params={"request_id": request_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_mitid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiMitidWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start MitID verification. + + Start a MitID sign-in (Danish Agency for Digital Government; DNK) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/mitid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the MitID identity. Defaults to DNK, the only country it serves. * `DNK` - DNK + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/mitid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_bankid_se( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryEe9Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start BankID (SWE) verification. + + Start a BankID (SWE) sign-in (Finansiell ID-Teknik / bank consortium; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/bankid-se/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the BankID identity. Defaults to SWE, the only country it serves. * `SWE` - SWE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/bankid-se/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_bankid_no( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryAfcEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start BankID (NOR) verification. + + Start a BankID (NOR) sign-in (BankID BankAxept AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/bankid-no/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the BankID identity. Defaults to NOR, the only country it serves. * `NOR` - NOR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/bankid-no/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_vipps( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryAfcEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Vipps verification. + + Start a Vipps sign-in (Vipps MobilePay / BankID NO; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/vipps/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Vipps identity. Defaults to NOR, the only country it serves. * `NOR` - NOR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/vipps/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_buypass( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryAfcEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Buypass ID verification. + + Start a Buypass ID sign-in (Buypass AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/buypass/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Buypass ID identity. Defaults to NOR, the only country it serves. * `NOR` - NOR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/buypass/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_itsme( + self, + *, + country: DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start itsme verification. + + Start a itsme sign-in (Belgian Mobile ID; BEL, LUX, NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/itsme/`` + + Args: + country: ISO 3166-1 alpha-3 country of the itsme identity. * `BEL` - BEL * `LUX` - LUX * `NLD` - NLD + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/itsme/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_idin( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiIdinWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start iDIN verification. + + Start a iDIN sign-in (Dutch banks (Currence iDIN); NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/idin/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the iDIN identity. Defaults to NLD, the only country it serves. * `NLD` - NLD + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/idin/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_ftn( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiFtnWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Finnish Trust Network verification. + + Start a Finnish Trust Network sign-in (Finnish banks and mobile operators (FTN); FIN) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/ftn/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Finnish Trust Network identity. Defaults to FIN, the only country it serves. * `FIN` - FIN + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/ftn/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_personalausweis( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Personalausweis verification. + + Start a Personalausweis sign-in (Bundesministerium des Innern (eID); DEU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/personalausweis/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Personalausweis identity. Defaults to DEU, the only country it serves. * `DEU` - DEU + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/personalausweis/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_frejaid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryEe9Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Freja eID verification. + + Start a Freja eID sign-in (Freja eID Group; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/frejaid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Freja eID identity. Defaults to SWE, the only country it serves. * `SWE` - SWE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/frejaid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_uae_pass( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start UAE PASS verification. + + Start a UAE PASS sign-in (UAE Digital Government Authority; ARE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/uae-pass/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the UAE PASS identity. Defaults to ARE, the only country it serves. * `ARE` - ARE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/uae-pass/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_govbr( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start gov.br verification. + + Start a gov.br sign-in (Governo Federal do Brasil; BRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/govbr/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the gov.br identity. Defaults to BRA, the only country it serves. * `BRA` - BRA + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/govbr/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_oneid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountry520Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start OneID verification. + + Start a OneID sign-in (OneID (UK bank-verified identity); GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/oneid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the OneID identity. Defaults to GBR, the only country it serves. * `GBR` - GBR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/oneid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_govuk_wallet( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountry520Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start GOV.UK Wallet verification. + + Start a GOV.UK Wallet sign-in (UK Government Digital Service; GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/govuk-wallet/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the GOV.UK Wallet identity. Defaults to GBR, the only country it serves. * `GBR` - GBR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/govuk-wallet/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_smart_id( + self, + *, + country: DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum, + personal_code: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Smart-ID verification. + + Start a Smart-ID sign-in (SK ID Solutions; EST, LVA, LTU, BEL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/smart-id/`` + + Args: + country: ISO 3166-1 alpha-3 country of the Smart-ID identity. * `EST` - EST * `LVA` - LVA * `LTU` - LTU * `BEL` - BEL + personal_code: The user's national personal identification code. It addresses the user's Smart-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/smart-id/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "personal_code": personal_code, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_mobile_id( + self, + *, + country: DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum, + personal_code: str, + phone_number: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Mobile-ID verification. + + Start a Mobile-ID sign-in (SK ID Solutions with the national mobile operators; EST, LTU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/mobile-id/`` + + Args: + country: ISO 3166-1 alpha-3 country of the Mobile-ID identity. * `EST` - EST * `LTU` - LTU + personal_code: The user's national personal identification code. It addresses the user's Mobile-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN. + phone_number: The Mobile-ID phone number in E.164 format (+372... or +370...). + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/mobile-id/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "personal_code": personal_code, + "phone_number": phone_number, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_idcard_ee( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Estonian ID-card verification. + + Start a Estonian ID-card sign-in (Police and Border Guard Board (Estonia); EST) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/idcard-ee/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Estonian ID-card identity. Defaults to EST, the only country it serves. * `EST` - EST + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/idcard-ee/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_eparaksts_mobile( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start eParaksts mobile verification. + + Start a eParaksts mobile sign-in (LVRTC (Latvian State Radio and Television Centre); LVA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/eparaksts-mobile/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the eParaksts mobile identity. Defaults to LVA, the only country it serves. * `LVA` - LVA + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/eparaksts-mobile/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_bankid_cz( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryE2dEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Bank iD verification. + + Start a Bank iD sign-in (Bankovní identita, a.s.; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/bankid-cz/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Bank iD identity. Defaults to CZE, the only country it serves. * `CZE` - CZE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/bankid-cz/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_mojeid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryE2dEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start MojeID verification. + + Start a MojeID sign-in (CZ.NIC; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/mojeid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the MojeID identity. Defaults to CZE, the only country it serves. * `CZE` - CZE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/mojeid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_diia( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Diia verification. + + Start a Diia sign-in (Ministry of Digital Transformation of Ukraine; UKR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/diia/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Diia identity. Defaults to UKR, the only country it serves. * `UKR` - UKR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/diia/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_franceconnect( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start FranceConnect verification. + + Start a FranceConnect sign-in (DINUM (French state); FRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/franceconnect/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the FranceConnect identity. Defaults to FRA, the only country it serves. * `FRA` - FRA + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/franceconnect/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_audkenni( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Auðkenni verification. + + Start a Auðkenni sign-in (Auðkenni (Icelandic electronic ID); ISL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/audkenni/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Auðkenni identity. Defaults to ISL, the only country it serves. * `ISL` - ISL + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/audkenni/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_connectid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + authorisation_server_id: str | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start ConnectID verification. + + Start a ConnectID sign-in (Australian Payments Plus; AUS) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/connectid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the ConnectID identity. Defaults to AUS, the only country it serves. * `AUS` - AUS + authorisation_server_id: Authorisation server ID selected with ConnectID's bank selector. Sandbox defaults to Mock Bank A. + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/connectid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "authorisation_server_id": authorisation_server_id, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def start_eudi( + self, + *, + country: DigitalIdWalletApiEudiWalletVerificationStartCountryEnum, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start EUDI Wallet verification. + + Start a EUDI Wallet sign-in (The user's own member state; the issuer differs per country; AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/eudi/`` + + Args: + country: ISO 3166-1 alpha-3 country of the EUDI Wallet identity. * `AUT` - AUT * `BEL` - BEL * `BGR` - BGR * `HRV` - HRV * `CYP` - CYP * `CZE` - CZE * `DNK` - DNK * `EST` - EST * `FIN` - FIN * `FRA` - FRA * `DEU` - DEU * `GRC` - GRC * `HUN` - HUN * `IRL` - IRL * `ITA` - ITA * `LVA` - LVA * `LTU` - LTU * `LUX` - LUX * `MLT` - MLT * `NLD` - NLD * `POL` - POL * `PRT` - PRT * `ROU` - ROU * `SVK` - SVK * `SVN` - SVN * `ESP` - ESP * `SWE` - SWE * `ISL` - ISL * `LIE` - LIE * `NOR` - NOR + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + self._client.request( + "post", + "/v3/id-verification/wallets/eudi/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncIdWallets(AsyncAPIResource): + """The ``id_wallets`` resource of the Didit API. + + Methods: ``cancel_verification``, ``list``, ``poll_verification``, ``retrieve_verification``, ``start_audkenni``, ``start_bankid_cz``, ``start_bankid_no``, ``start_bankid_se``, ``start_buypass``, ``start_connectid``, ``start_diia``, ``start_eparaksts_mobile``, ``start_eudi``, ``start_franceconnect``, ``start_frejaid``, ``start_ftn``, ``start_govbr``, ``start_govuk_wallet``, ``start_idcard_ee``, ``start_idin``, ``start_itsme``, ``start_mitid``, ``start_mobile_id``, ``start_mojeid``, ``start_oneid``, ``start_personalausweis``, ``start_smart_id``, ``start_uae_pass``, ``start_vipps``. + """ + + @cached_property + def with_raw_response(self) -> AsyncIdWalletsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncIdWalletsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncIdWalletsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncIdWalletsWithStreamingResponse(self) + + async def list( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletCatalogResponse: + """List Digital ID wallets. + + Every Digital ID wallet with its start endpoint, countries, required fields and whether this application can start it in this environment now. Listed does not mean live. + + ``GET /v3/id-verification/wallets/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletCatalogResponse", + await self._client.request( + "get", + "/v3/id-verification/wallets/", + cast_to=DigitalIdWalletApiWalletCatalogResponse, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_verification( + self, + request_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Get a wallet verification. + + Status and result of a wallet verification: the normalized identity and the wallet's own attributes once verified, the error code once declined, the next action while pending. A verification past expires_at is decided timeout. + + ``GET /v3/id-verification/wallets/verifications/{request_id}/`` + + Args: + request_id: The request_id the start endpoint returned. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "get", + "/v3/id-verification/wallets/verifications/{request_id}/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + path_params={"request_id": request_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def poll_verification( + self, + request_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Poll a wallet verification. + + Smart-ID and Mobile-ID: ask the provider whether the user confirmed, refused or let the prompt lapse, and verify the signed identity when they confirmed. Redirect wallets complete through the user's browser, so for them this only reads the verification back. Answers the verification as it is afterwards. + + ``POST /v3/id-verification/wallets/verifications/{request_id}/poll/`` + + Args: + request_id: The request_id the start endpoint returned. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/verifications/{request_id}/poll/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + path_params={"request_id": request_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def cancel_verification( + self, + request_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Cancel a wallet verification. + + Withdraw a pending verification: it is declined as cancelled and never billed. A decided one is returned unchanged. Smart-ID and Mobile-ID: a cancel that arrives while a poll is checking the sign-in waits a few seconds for that poll, then declines it, or returns it verified if the user had already confirmed. A verification the user is completing right now cannot be cancelled (409 verification_in_progress with Retry-After): retry after that many seconds to read its outcome. + + ``POST /v3/id-verification/wallets/verifications/{request_id}/cancel/`` + + Args: + request_id: The request_id the start endpoint returned. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/verifications/{request_id}/cancel/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + path_params={"request_id": request_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_mitid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiMitidWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start MitID verification. + + Start a MitID sign-in (Danish Agency for Digital Government; DNK) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/mitid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the MitID identity. Defaults to DNK, the only country it serves. * `DNK` - DNK + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/mitid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_bankid_se( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryEe9Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start BankID (SWE) verification. + + Start a BankID (SWE) sign-in (Finansiell ID-Teknik / bank consortium; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/bankid-se/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the BankID identity. Defaults to SWE, the only country it serves. * `SWE` - SWE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/bankid-se/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_bankid_no( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryAfcEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start BankID (NOR) verification. + + Start a BankID (NOR) sign-in (BankID BankAxept AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/bankid-no/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the BankID identity. Defaults to NOR, the only country it serves. * `NOR` - NOR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/bankid-no/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_vipps( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryAfcEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Vipps verification. + + Start a Vipps sign-in (Vipps MobilePay / BankID NO; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/vipps/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Vipps identity. Defaults to NOR, the only country it serves. * `NOR` - NOR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/vipps/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_buypass( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryAfcEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Buypass ID verification. + + Start a Buypass ID sign-in (Buypass AS; NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/buypass/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Buypass ID identity. Defaults to NOR, the only country it serves. * `NOR` - NOR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/buypass/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_itsme( + self, + *, + country: DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start itsme verification. + + Start a itsme sign-in (Belgian Mobile ID; BEL, LUX, NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/itsme/`` + + Args: + country: ISO 3166-1 alpha-3 country of the itsme identity. * `BEL` - BEL * `LUX` - LUX * `NLD` - NLD + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/itsme/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_idin( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiIdinWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start iDIN verification. + + Start a iDIN sign-in (Dutch banks (Currence iDIN); NLD) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/idin/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the iDIN identity. Defaults to NLD, the only country it serves. * `NLD` - NLD + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/idin/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_ftn( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiFtnWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Finnish Trust Network verification. + + Start a Finnish Trust Network sign-in (Finnish banks and mobile operators (FTN); FIN) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/ftn/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Finnish Trust Network identity. Defaults to FIN, the only country it serves. * `FIN` - FIN + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/ftn/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_personalausweis( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Personalausweis verification. + + Start a Personalausweis sign-in (Bundesministerium des Innern (eID); DEU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/personalausweis/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Personalausweis identity. Defaults to DEU, the only country it serves. * `DEU` - DEU + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/personalausweis/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_frejaid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryEe9Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Freja eID verification. + + Start a Freja eID sign-in (Freja eID Group; SWE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/frejaid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Freja eID identity. Defaults to SWE, the only country it serves. * `SWE` - SWE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/frejaid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_uae_pass( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start UAE PASS verification. + + Start a UAE PASS sign-in (UAE Digital Government Authority; ARE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/uae-pass/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the UAE PASS identity. Defaults to ARE, the only country it serves. * `ARE` - ARE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/uae-pass/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_govbr( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start gov.br verification. + + Start a gov.br sign-in (Governo Federal do Brasil; BRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/govbr/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the gov.br identity. Defaults to BRA, the only country it serves. * `BRA` - BRA + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/govbr/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_oneid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountry520Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start OneID verification. + + Start a OneID sign-in (OneID (UK bank-verified identity); GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/oneid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the OneID identity. Defaults to GBR, the only country it serves. * `GBR` - GBR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/oneid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_govuk_wallet( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountry520Enum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start GOV.UK Wallet verification. + + Start a GOV.UK Wallet sign-in (UK Government Digital Service; GBR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/govuk-wallet/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the GOV.UK Wallet identity. Defaults to GBR, the only country it serves. * `GBR` - GBR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/govuk-wallet/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_smart_id( + self, + *, + country: DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum, + personal_code: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Smart-ID verification. + + Start a Smart-ID sign-in (SK ID Solutions; EST, LVA, LTU, BEL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/smart-id/`` + + Args: + country: ISO 3166-1 alpha-3 country of the Smart-ID identity. * `EST` - EST * `LVA` - LVA * `LTU` - LTU * `BEL` - BEL + personal_code: The user's national personal identification code. It addresses the user's Smart-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/smart-id/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "personal_code": personal_code, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_mobile_id( + self, + *, + country: DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum, + personal_code: str, + phone_number: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Mobile-ID verification. + + Start a Mobile-ID sign-in (SK ID Solutions with the national mobile operators; EST, LTU) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=confirm_in_app with the comparison code to show the user; the user confirms it and enters their PIN in the app (never collect the PIN). Poll POST /v3/id-verification/wallets/verifications/{request_id}/poll/ every poll_interval_seconds, or rely on the webhook: Didit polls in the background too. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/mobile-id/`` + + Args: + country: ISO 3166-1 alpha-3 country of the Mobile-ID identity. * `EST` - EST * `LTU` - LTU + personal_code: The user's national personal identification code. It addresses the user's Mobile-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN. + phone_number: The Mobile-ID phone number in E.164 format (+372... or +370...). + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/mobile-id/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "personal_code": personal_code, + "phone_number": phone_number, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_idcard_ee( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Estonian ID-card verification. + + Start a Estonian ID-card sign-in (Police and Border Guard Board (Estonia); EST) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/idcard-ee/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Estonian ID-card identity. Defaults to EST, the only country it serves. * `EST` - EST + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/idcard-ee/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_eparaksts_mobile( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start eParaksts mobile verification. + + Start a eParaksts mobile sign-in (LVRTC (Latvian State Radio and Television Centre); LVA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/eparaksts-mobile/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the eParaksts mobile identity. Defaults to LVA, the only country it serves. * `LVA` - LVA + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/eparaksts-mobile/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_bankid_cz( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryE2dEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Bank iD verification. + + Start a Bank iD sign-in (Bankovní identita, a.s.; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/bankid-cz/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Bank iD identity. Defaults to CZE, the only country it serves. * `CZE` - CZE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/bankid-cz/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_mojeid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiCountryE2dEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start MojeID verification. + + Start a MojeID sign-in (CZ.NIC; CZE) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/mojeid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the MojeID identity. Defaults to CZE, the only country it serves. * `CZE` - CZE + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/mojeid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_diia( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Diia verification. + + Start a Diia sign-in (Ministry of Digital Transformation of Ukraine; UKR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/diia/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Diia identity. Defaults to UKR, the only country it serves. * `UKR` - UKR + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/diia/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_franceconnect( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start FranceConnect verification. + + Start a FranceConnect sign-in (DINUM (French state); FRA) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/franceconnect/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the FranceConnect identity. Defaults to FRA, the only country it serves. * `FRA` - FRA + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/franceconnect/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_audkenni( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start Auðkenni verification. + + Start a Auðkenni sign-in (Auðkenni (Icelandic electronic ID); ISL) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/audkenni/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the Auðkenni identity. Defaults to ISL, the only country it serves. * `ISL` - ISL + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/audkenni/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_connectid( + self, + *, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + country: DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum + | NotGiven = NOT_GIVEN, + authorisation_server_id: str | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start ConnectID verification. + + Start a ConnectID sign-in (Australian Payments Plus; AUS) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/connectid/`` + + Args: + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + country: ISO 3166-1 alpha-3 country of the ConnectID identity. Defaults to AUS, the only country it serves. * `AUS` - AUS + authorisation_server_id: Authorisation server ID selected with ConnectID's bank selector. Sandbox defaults to Mock Bank A. + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/connectid/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "country": country, + "authorisation_server_id": authorisation_server_id, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def start_eudi( + self, + *, + country: DigitalIdWalletApiEudiWalletVerificationStartCountryEnum, + return_url: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + device: DigitalIdWalletApiDeviceEnum | NotGiven = NOT_GIVEN, + idempotency_key: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> DigitalIdWalletApiWalletVerificationResponse: + """Start EUDI Wallet verification. + + Start a EUDI Wallet sign-in (The user's own member state; the issuer differs per country; AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR) without any workflow. Billed once, only when the identity is verified. The response carries next_action.type=redirect with the URL to send the user's browser to; the user returns to return_url with request_id and result appended. Read the outcome with GET /v3/id-verification/wallets/verifications/{request_id}/ or from the webhook, never from the URL. Sandbox applications get a deterministic simulation (simulated: true) that never contacts the provider. + + ``POST /v3/id-verification/wallets/eudi/`` + + Args: + country: ISO 3166-1 alpha-3 country of the EUDI Wallet identity. * `AUT` - AUT * `BEL` - BEL * `BGR` - BGR * `HRV` - HRV * `CYP` - CYP * `CZE` - CZE * `DNK` - DNK * `EST` - EST * `FIN` - FIN * `FRA` - FRA * `DEU` - DEU * `GRC` - GRC * `HUN` - HUN * `IRL` - IRL * `ITA` - ITA * `LVA` - LVA * `LTU` - LTU * `LUX` - LUX * `MLT` - MLT * `NLD` - NLD * `POL` - POL * `PRT` - PRT * `ROU` - ROU * `SVK` - SVK * `SVN` - SVN * `ESP` - ESP * `SWE` - SWE * `ISL` - ISL * `LIE` - LIE * `NOR` - NOR + return_url: https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook. + vendor_data: Your own reference for this verification (e.g. your user id). Returned in results and webhooks. + metadata: Optional JSON object stored with the verification and returned in results and webhooks. + sandbox_scenario: Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/. + device: same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). * `same_device` - same_device * `cross_device` - cross_device + idempotency_key: Sent as the ``Idempotency-Key`` header; a retry with the same key returns the first result instead of acting twice. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "DigitalIdWalletApiWalletVerificationResponse", + await self._client.request( + "post", + "/v3/id-verification/wallets/eudi/", + cast_to=DigitalIdWalletApiWalletVerificationResponse, + auth="api_key", + body={ + "country": country, + "return_url": return_url, + "vendor_data": vendor_data, + "metadata": metadata, + "sandbox_scenario": sandbox_scenario, + "device": device, + }, + headers={"Idempotency-Key": idempotency_key}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class IdWalletsWithRawResponse: + def __init__(self, resource: IdWallets) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.retrieve_verification = to_raw_response_wrapper(resource.retrieve_verification) + self.poll_verification = to_raw_response_wrapper(resource.poll_verification) + self.cancel_verification = to_raw_response_wrapper(resource.cancel_verification) + self.start_mitid = to_raw_response_wrapper(resource.start_mitid) + self.start_bankid_se = to_raw_response_wrapper(resource.start_bankid_se) + self.start_bankid_no = to_raw_response_wrapper(resource.start_bankid_no) + self.start_vipps = to_raw_response_wrapper(resource.start_vipps) + self.start_buypass = to_raw_response_wrapper(resource.start_buypass) + self.start_itsme = to_raw_response_wrapper(resource.start_itsme) + self.start_idin = to_raw_response_wrapper(resource.start_idin) + self.start_ftn = to_raw_response_wrapper(resource.start_ftn) + self.start_personalausweis = to_raw_response_wrapper(resource.start_personalausweis) + self.start_frejaid = to_raw_response_wrapper(resource.start_frejaid) + self.start_uae_pass = to_raw_response_wrapper(resource.start_uae_pass) + self.start_govbr = to_raw_response_wrapper(resource.start_govbr) + self.start_oneid = to_raw_response_wrapper(resource.start_oneid) + self.start_govuk_wallet = to_raw_response_wrapper(resource.start_govuk_wallet) + self.start_smart_id = to_raw_response_wrapper(resource.start_smart_id) + self.start_mobile_id = to_raw_response_wrapper(resource.start_mobile_id) + self.start_idcard_ee = to_raw_response_wrapper(resource.start_idcard_ee) + self.start_eparaksts_mobile = to_raw_response_wrapper(resource.start_eparaksts_mobile) + self.start_bankid_cz = to_raw_response_wrapper(resource.start_bankid_cz) + self.start_mojeid = to_raw_response_wrapper(resource.start_mojeid) + self.start_diia = to_raw_response_wrapper(resource.start_diia) + self.start_franceconnect = to_raw_response_wrapper(resource.start_franceconnect) + self.start_audkenni = to_raw_response_wrapper(resource.start_audkenni) + self.start_connectid = to_raw_response_wrapper(resource.start_connectid) + self.start_eudi = to_raw_response_wrapper(resource.start_eudi) + + +class AsyncIdWalletsWithRawResponse: + def __init__(self, resource: AsyncIdWallets) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.retrieve_verification = async_to_raw_response_wrapper(resource.retrieve_verification) + self.poll_verification = async_to_raw_response_wrapper(resource.poll_verification) + self.cancel_verification = async_to_raw_response_wrapper(resource.cancel_verification) + self.start_mitid = async_to_raw_response_wrapper(resource.start_mitid) + self.start_bankid_se = async_to_raw_response_wrapper(resource.start_bankid_se) + self.start_bankid_no = async_to_raw_response_wrapper(resource.start_bankid_no) + self.start_vipps = async_to_raw_response_wrapper(resource.start_vipps) + self.start_buypass = async_to_raw_response_wrapper(resource.start_buypass) + self.start_itsme = async_to_raw_response_wrapper(resource.start_itsme) + self.start_idin = async_to_raw_response_wrapper(resource.start_idin) + self.start_ftn = async_to_raw_response_wrapper(resource.start_ftn) + self.start_personalausweis = async_to_raw_response_wrapper(resource.start_personalausweis) + self.start_frejaid = async_to_raw_response_wrapper(resource.start_frejaid) + self.start_uae_pass = async_to_raw_response_wrapper(resource.start_uae_pass) + self.start_govbr = async_to_raw_response_wrapper(resource.start_govbr) + self.start_oneid = async_to_raw_response_wrapper(resource.start_oneid) + self.start_govuk_wallet = async_to_raw_response_wrapper(resource.start_govuk_wallet) + self.start_smart_id = async_to_raw_response_wrapper(resource.start_smart_id) + self.start_mobile_id = async_to_raw_response_wrapper(resource.start_mobile_id) + self.start_idcard_ee = async_to_raw_response_wrapper(resource.start_idcard_ee) + self.start_eparaksts_mobile = async_to_raw_response_wrapper(resource.start_eparaksts_mobile) + self.start_bankid_cz = async_to_raw_response_wrapper(resource.start_bankid_cz) + self.start_mojeid = async_to_raw_response_wrapper(resource.start_mojeid) + self.start_diia = async_to_raw_response_wrapper(resource.start_diia) + self.start_franceconnect = async_to_raw_response_wrapper(resource.start_franceconnect) + self.start_audkenni = async_to_raw_response_wrapper(resource.start_audkenni) + self.start_connectid = async_to_raw_response_wrapper(resource.start_connectid) + self.start_eudi = async_to_raw_response_wrapper(resource.start_eudi) + + +class IdWalletsWithStreamingResponse: + def __init__(self, resource: IdWallets) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.retrieve_verification = to_streamed_response_wrapper(resource.retrieve_verification) + self.poll_verification = to_streamed_response_wrapper(resource.poll_verification) + self.cancel_verification = to_streamed_response_wrapper(resource.cancel_verification) + self.start_mitid = to_streamed_response_wrapper(resource.start_mitid) + self.start_bankid_se = to_streamed_response_wrapper(resource.start_bankid_se) + self.start_bankid_no = to_streamed_response_wrapper(resource.start_bankid_no) + self.start_vipps = to_streamed_response_wrapper(resource.start_vipps) + self.start_buypass = to_streamed_response_wrapper(resource.start_buypass) + self.start_itsme = to_streamed_response_wrapper(resource.start_itsme) + self.start_idin = to_streamed_response_wrapper(resource.start_idin) + self.start_ftn = to_streamed_response_wrapper(resource.start_ftn) + self.start_personalausweis = to_streamed_response_wrapper(resource.start_personalausweis) + self.start_frejaid = to_streamed_response_wrapper(resource.start_frejaid) + self.start_uae_pass = to_streamed_response_wrapper(resource.start_uae_pass) + self.start_govbr = to_streamed_response_wrapper(resource.start_govbr) + self.start_oneid = to_streamed_response_wrapper(resource.start_oneid) + self.start_govuk_wallet = to_streamed_response_wrapper(resource.start_govuk_wallet) + self.start_smart_id = to_streamed_response_wrapper(resource.start_smart_id) + self.start_mobile_id = to_streamed_response_wrapper(resource.start_mobile_id) + self.start_idcard_ee = to_streamed_response_wrapper(resource.start_idcard_ee) + self.start_eparaksts_mobile = to_streamed_response_wrapper(resource.start_eparaksts_mobile) + self.start_bankid_cz = to_streamed_response_wrapper(resource.start_bankid_cz) + self.start_mojeid = to_streamed_response_wrapper(resource.start_mojeid) + self.start_diia = to_streamed_response_wrapper(resource.start_diia) + self.start_franceconnect = to_streamed_response_wrapper(resource.start_franceconnect) + self.start_audkenni = to_streamed_response_wrapper(resource.start_audkenni) + self.start_connectid = to_streamed_response_wrapper(resource.start_connectid) + self.start_eudi = to_streamed_response_wrapper(resource.start_eudi) + + +class AsyncIdWalletsWithStreamingResponse: + def __init__(self, resource: AsyncIdWallets) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.retrieve_verification = async_to_streamed_response_wrapper( + resource.retrieve_verification + ) + self.poll_verification = async_to_streamed_response_wrapper(resource.poll_verification) + self.cancel_verification = async_to_streamed_response_wrapper(resource.cancel_verification) + self.start_mitid = async_to_streamed_response_wrapper(resource.start_mitid) + self.start_bankid_se = async_to_streamed_response_wrapper(resource.start_bankid_se) + self.start_bankid_no = async_to_streamed_response_wrapper(resource.start_bankid_no) + self.start_vipps = async_to_streamed_response_wrapper(resource.start_vipps) + self.start_buypass = async_to_streamed_response_wrapper(resource.start_buypass) + self.start_itsme = async_to_streamed_response_wrapper(resource.start_itsme) + self.start_idin = async_to_streamed_response_wrapper(resource.start_idin) + self.start_ftn = async_to_streamed_response_wrapper(resource.start_ftn) + self.start_personalausweis = async_to_streamed_response_wrapper( + resource.start_personalausweis + ) + self.start_frejaid = async_to_streamed_response_wrapper(resource.start_frejaid) + self.start_uae_pass = async_to_streamed_response_wrapper(resource.start_uae_pass) + self.start_govbr = async_to_streamed_response_wrapper(resource.start_govbr) + self.start_oneid = async_to_streamed_response_wrapper(resource.start_oneid) + self.start_govuk_wallet = async_to_streamed_response_wrapper(resource.start_govuk_wallet) + self.start_smart_id = async_to_streamed_response_wrapper(resource.start_smart_id) + self.start_mobile_id = async_to_streamed_response_wrapper(resource.start_mobile_id) + self.start_idcard_ee = async_to_streamed_response_wrapper(resource.start_idcard_ee) + self.start_eparaksts_mobile = async_to_streamed_response_wrapper( + resource.start_eparaksts_mobile + ) + self.start_bankid_cz = async_to_streamed_response_wrapper(resource.start_bankid_cz) + self.start_mojeid = async_to_streamed_response_wrapper(resource.start_mojeid) + self.start_diia = async_to_streamed_response_wrapper(resource.start_diia) + self.start_franceconnect = async_to_streamed_response_wrapper(resource.start_franceconnect) + self.start_audkenni = async_to_streamed_response_wrapper(resource.start_audkenni) + self.start_connectid = async_to_streamed_response_wrapper(resource.start_connectid) + self.start_eudi = async_to_streamed_response_wrapper(resource.start_eudi) diff --git a/src/didit/resources/ip.py b/src/didit/resources/ip.py new file mode 100644 index 0000000..6f64ac6 --- /dev/null +++ b/src/didit/resources/ip.py @@ -0,0 +1,212 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``ip`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.ip_risk_response import IPRiskResponse + +__all__ = [ + "Ip", + "AsyncIp", + "IpWithRawResponse", + "AsyncIpWithRawResponse", + "IpWithStreamingResponse", + "AsyncIpWithStreamingResponse", +] + + +class Ip(SyncAPIResource): + """The ``ip`` resource of the Didit API. + + Methods: ``assess_risk``. + """ + + @cached_property + def with_raw_response(self) -> IpWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return IpWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> IpWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return IpWithStreamingResponse(self) + + def assess_risk( + self, + *, + ip_address: str, + claimed_country: str | NotGiven = NOT_GIVEN, + user_agent: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IPRiskResponse: + """Risk-score an IP address. + + Return geolocation, network ownership, VPN/Tor and data-center flags, threat data, and `country_mismatch` for a supplied IP address. The endpoint creates an API session and returns its id as `request_id`. + + **Decision.** `status` is `In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise it is `Approved`. + + **Best effort.** IP enrichment is best effort. If no enrichment source returns usable data, the endpoint returns an approved response with `ip.ip_address: null`, `ip.country_mismatch: null`, and an empty `ip.threat` object. + + **Billing.** Live calls use IP Risk API pricing. The request is billable once the lookup starts. + + **Sandbox.** Sandbox keys validate the request and return deterministic approved data without consuming credits, creating a session, or contributing to Networks. + + **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + + ``POST /v3/ip/risk/`` + + Args: + ip_address: IPv4 or IPv6 address to score. + claimed_country: Optional ISO 3166-1 alpha-2 or alpha-3 country claimed by the user or session. Used to calculate `country_mismatch`. + user_agent: User agent observed by your backend. Stored as request context; it does not provide device fingerprinting. + vendor_data: Your stable reference for this request. Stored on the API session and echoed in the response. + metadata: Your non-sensitive JSON metadata. Stored on the API session and echoed in the response. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "IPRiskResponse", + self._client.request( + "post", + "/v3/ip/risk/", + cast_to=IPRiskResponse, + auth="api_key", + body={ + "ip_address": ip_address, + "claimed_country": claimed_country, + "user_agent": user_agent, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncIp(AsyncAPIResource): + """The ``ip`` resource of the Didit API. + + Methods: ``assess_risk``. + """ + + @cached_property + def with_raw_response(self) -> AsyncIpWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncIpWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncIpWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncIpWithStreamingResponse(self) + + async def assess_risk( + self, + *, + ip_address: str, + claimed_country: str | NotGiven = NOT_GIVEN, + user_agent: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> IPRiskResponse: + """Risk-score an IP address. + + Return geolocation, network ownership, VPN/Tor and data-center flags, threat data, and `country_mismatch` for a supplied IP address. The endpoint creates an API session and returns its id as `request_id`. + + **Decision.** `status` is `In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise it is `Approved`. + + **Best effort.** IP enrichment is best effort. If no enrichment source returns usable data, the endpoint returns an approved response with `ip.ip_address: null`, `ip.country_mismatch: null`, and an empty `ip.threat` object. + + **Billing.** Live calls use IP Risk API pricing. The request is billable once the lookup starts. + + **Sandbox.** Sandbox keys validate the request and return deterministic approved data without consuming credits, creating a session, or contributing to Networks. + + **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + + ``POST /v3/ip/risk/`` + + Args: + ip_address: IPv4 or IPv6 address to score. + claimed_country: Optional ISO 3166-1 alpha-2 or alpha-3 country claimed by the user or session. Used to calculate `country_mismatch`. + user_agent: User agent observed by your backend. Stored as request context; it does not provide device fingerprinting. + vendor_data: Your stable reference for this request. Stored on the API session and echoed in the response. + metadata: Your non-sensitive JSON metadata. Stored on the API session and echoed in the response. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "IPRiskResponse", + await self._client.request( + "post", + "/v3/ip/risk/", + cast_to=IPRiskResponse, + auth="api_key", + body={ + "ip_address": ip_address, + "claimed_country": claimed_country, + "user_agent": user_agent, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class IpWithRawResponse: + def __init__(self, resource: Ip) -> None: + self._resource = resource + self.assess_risk = to_raw_response_wrapper(resource.assess_risk) + + +class AsyncIpWithRawResponse: + def __init__(self, resource: AsyncIp) -> None: + self._resource = resource + self.assess_risk = async_to_raw_response_wrapper(resource.assess_risk) + + +class IpWithStreamingResponse: + def __init__(self, resource: Ip) -> None: + self._resource = resource + self.assess_risk = to_streamed_response_wrapper(resource.assess_risk) + + +class AsyncIpWithStreamingResponse: + def __init__(self, resource: AsyncIp) -> None: + self._resource = resource + self.assess_risk = async_to_streamed_response_wrapper(resource.assess_risk) diff --git a/src/didit/resources/kyb.py b/src/didit/resources/kyb.py new file mode 100644 index 0000000..fe58951 --- /dev/null +++ b/src/didit/resources/kyb.py @@ -0,0 +1,574 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``kyb`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse, + KybListShareholdersResponse202, +) +from didit.types.kyb.list_ubos_response import KybListUbosResponse, KybListUbosResponse202 +from didit.types.kyb.search_response import KybSearchResponse +from didit.types.kyb.select_response import KybSelectResponse + +__all__ = [ + "Kyb", + "AsyncKyb", + "KybWithRawResponse", + "AsyncKybWithRawResponse", + "KybWithStreamingResponse", + "AsyncKybWithStreamingResponse", +] + + +class Kyb(SyncAPIResource): + """The ``kyb`` resource of the Didit API. + + Methods: ``list_shareholders``, ``list_ubos``, ``search``, ``select``. + """ + + @cached_property + def with_raw_response(self) -> KybWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return KybWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> KybWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return KybWithStreamingResponse(self) + + def search( + self, + *, + country_code: str, + name: str | NotGiven = NOT_GIVEN, + registration_number: str | NotGiven = NOT_GIVEN, + search_type: Literal["contains", "start_with", "fuzzy"] | str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + webhook_url: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybSearchResponse: + """KYB registry company search (standalone). + + Search official company registries for candidate companies by name and/or registration number. This is step 1 of the two-step registry flow: **search** returns lightweight candidates, then `POST /v3/kyb/select/` retrieves the full profile of one candidate. + + **Search billing.** Successful searches using a paid company-data source cost USD 0.50 per request, regardless of candidate count. Empty results, failures and results served only by other sources are not charged. The applicable price is reserved before searching; insufficient balance returns 403 with code `insufficient_credits`. Contract rates apply where agreed, and an unconfigured contract search rate returns 403. Sandbox searches are free. Repeating a successful search can incur another charge; callback delivery does not add a charge. Profile retrieval is billed separately. This search fee also applies to v2. Candidates always come back with `fetch_status: "pending"`: the full company data is only fetched (and billed) on select. + + **`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it. + + **Two modes.** + - *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`). + - *Asynchronous:* pass `webhook_url` and the API returns immediately - usually with `search_status: "pending"` and an empty candidate list - then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{"event_id", "event_type": "kyb.registry_search.resolved", "request_id", "vendor_data", "metadata", "search_status", "search_resolved", "kyb_registry", "timestamp", "created_at"}` - note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint's ISO `created_at` - and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`). + + **Country codes.** ISO 3166-1 alpha-2 (`GB`, `DE`), plus `XX-YY` subdivision codes where registries are regional - e.g. `US-CA` for California. + + **Sandbox.** Keys from sandbox applications return one static candidate without contacting any registry. + + ``POST /v3/kyb/search/`` + + Args: + country_code: ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision - a bare `US` is rejected. Case-insensitive; normalized to upper case. + name: Company name to search for. Either `name` or `registration_number` is required. + registration_number: Registry registration number to search for. Either `name` or `registration_number` is required. + search_type: Name-matching strategy applied by the registry search. + vendor_data: Your identifier for this lookup; echoed back and included in the webhook callback. + metadata: Free-form JSON; echoed back and included in the webhook callback. + webhook_url: Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybSearchResponse", + self._client.request( + "post", + "/v3/kyb/search/", + cast_to=KybSearchResponse, + auth="api_key", + body={ + "country_code": country_code, + "name": name, + "registration_number": registration_number, + "search_type": search_type, + "vendor_data": vendor_data, + "metadata": metadata, + "webhook_url": webhook_url, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def select( + self, + *, + kyb_response_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybSelectResponse: + """KYB registry company select - Lite tier (standalone). + + Retrieve the **Lite** registry profile of one candidate returned by `POST /v3/kyb/search/` - company details, registered address, identifiers, status, activity and the officers, plus the raw registry payload. Shareholders and ultimate beneficial owners are sold by `POST /v3/kyb/shareholders/` and `POST /v3/kyb/ubo/`. + + **Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The handle is ephemeral and scoped to that search - select promptly, and re-search for a fresh handle rather than storing it. + + **Billing.** Each select is billed at the country's Lite price (any search fee is separate); see [KYB registry pricing](/getting-started/kyb-registry-pricing). The balance is checked before any registry call (`403` when short) and the charge is recorded against the created business session. + + **Persistence is mandatory.** Every select creates a business session (its id is the `request_id`) so billing and retrieval can be tracked; it appears in the console. `save_api_request` is accepted for symmetry but must be `true` (the default) - sending `false` returns `400`. + + ``POST /v3/kyb/select/`` + + Args: + kyb_response_id: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it. + vendor_data: Your identifier for this company; echoed back and stored with the business session. + metadata: Free-form JSON stored with the business session and echoed back. + save_api_request: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybSelectResponse", + self._client.request( + "post", + "/v3/kyb/select/", + cast_to=KybSelectResponse, + auth="api_key", + body={ + "kyb_response_id": kyb_response_id, + "vendor_data": vendor_data, + "metadata": metadata, + "save_api_request": save_api_request, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_shareholders( + self, + *, + kyb_response_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybListShareholdersResponse | KybListShareholdersResponse202: + """KYB registry company with shareholders (standalone). + + Retrieve the Lite profile **plus the direct shareholders** (natural persons and corporate entities, with their ownership bands) of one candidate returned by `POST /v3/kyb/search/`. + + **Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost. + + **Billing.** Billed at the country's Shareholders price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). When the registry holds no shareholder record for the company (or the country offers no ownership data at all) the Lite profile is delivered and billed instead: read `registry_tier_delivered` and `registry_ownership_status`. The balance is checked against the Shareholders price before any registry call (`403` when short). + + **Availability.** v3 only. Every call creates a business session (its id is the `request_id`). + + **Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request. + + ``POST /v3/kyb/shareholders/`` + + Args: + kyb_response_id: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it. + vendor_data: Your identifier for this company; echoed back and stored with the business session. + metadata: Free-form JSON stored with the business session and echoed back. + save_api_request: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybListShareholdersResponse | KybListShareholdersResponse202", + self._client.request( + "post", + "/v3/kyb/shareholders/", + cast_to={200: KybListShareholdersResponse, 202: KybListShareholdersResponse202}, + auth="api_key", + body={ + "kyb_response_id": kyb_response_id, + "vendor_data": vendor_data, + "metadata": metadata, + "save_api_request": save_api_request, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_ubos( + self, + *, + kyb_response_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybListUbosResponse | KybListUbosResponse202: + """KYB registry company with ultimate beneficial owners (standalone). + + Retrieve the Lite profile, the direct shareholders **and the ultimate beneficial owners** resolved through the ownership chain (`registry_data.ubo_resolution` carries the corporate parent chain) of one candidate returned by `POST /v3/kyb/search/`. + + **Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost. + + **Billing.** Billed at the country's UBO price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). The tier delivered is the highest the country and the registry can serve: Shareholders when the resolved report names no natural person as a beneficial owner, Lite when there is no ownership record at all; `registry_tier_delivered` names what was billed. The balance is checked against the UBO price before any registry call (`403` when short). + + **Availability.** v3 only. Every call creates a business session (its id is the `request_id`). + + **Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request. + + ``POST /v3/kyb/ubo/`` + + Args: + kyb_response_id: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it. + vendor_data: Your identifier for this company; echoed back and stored with the business session. + metadata: Free-form JSON stored with the business session and echoed back. + save_api_request: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybListUbosResponse | KybListUbosResponse202", + self._client.request( + "post", + "/v3/kyb/ubo/", + cast_to={200: KybListUbosResponse, 202: KybListUbosResponse202}, + auth="api_key", + body={ + "kyb_response_id": kyb_response_id, + "vendor_data": vendor_data, + "metadata": metadata, + "save_api_request": save_api_request, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncKyb(AsyncAPIResource): + """The ``kyb`` resource of the Didit API. + + Methods: ``list_shareholders``, ``list_ubos``, ``search``, ``select``. + """ + + @cached_property + def with_raw_response(self) -> AsyncKybWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncKybWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncKybWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncKybWithStreamingResponse(self) + + async def search( + self, + *, + country_code: str, + name: str | NotGiven = NOT_GIVEN, + registration_number: str | NotGiven = NOT_GIVEN, + search_type: Literal["contains", "start_with", "fuzzy"] | str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + webhook_url: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybSearchResponse: + """KYB registry company search (standalone). + + Search official company registries for candidate companies by name and/or registration number. This is step 1 of the two-step registry flow: **search** returns lightweight candidates, then `POST /v3/kyb/select/` retrieves the full profile of one candidate. + + **Search billing.** Successful searches using a paid company-data source cost USD 0.50 per request, regardless of candidate count. Empty results, failures and results served only by other sources are not charged. The applicable price is reserved before searching; insufficient balance returns 403 with code `insufficient_credits`. Contract rates apply where agreed, and an unconfigured contract search rate returns 403. Sandbox searches are free. Repeating a successful search can incur another charge; callback delivery does not add a charge. Profile retrieval is billed separately. This search fee also applies to v2. Candidates always come back with `fetch_status: "pending"`: the full company data is only fetched (and billed) on select. + + **`kyb_response_id` is ephemeral.** It is a per-search handle, not a stable company id. Select promptly after searching and re-search to get a fresh handle instead of storing it. + + **Two modes.** + - *Synchronous (default):* the API polls the registry until the search resolves and returns the final candidate list (`search_resolved: true`). + - *Asynchronous:* pass `webhook_url` and the API returns immediately - usually with `search_status: "pending"` and an empty candidate list - then POSTs a `kyb.registry_search.resolved` callback to your URL when the registry finishes. The callback body is `{"event_id", "event_type": "kyb.registry_search.resolved", "request_id", "vendor_data", "metadata", "search_status", "search_resolved", "kyb_registry", "timestamp", "created_at"}` - note `timestamp` and `created_at` in the callback are epoch integers, unlike this endpoint's ISO `created_at` - and is sent **unsigned** (header `X-Didit-Unsigned-Callback: true`, no `X-Signature`). + + **Country codes.** ISO 3166-1 alpha-2 (`GB`, `DE`), plus `XX-YY` subdivision codes where registries are regional - e.g. `US-CA` for California. + + **Sandbox.** Keys from sandbox applications return one static candidate without contacting any registry. + + ``POST /v3/kyb/search/`` + + Args: + country_code: ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision - a bare `US` is rejected. Case-insensitive; normalized to upper case. + name: Company name to search for. Either `name` or `registration_number` is required. + registration_number: Registry registration number to search for. Either `name` or `registration_number` is required. + search_type: Name-matching strategy applied by the registry search. + vendor_data: Your identifier for this lookup; echoed back and included in the webhook callback. + metadata: Free-form JSON; echoed back and included in the webhook callback. + webhook_url: Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybSearchResponse", + await self._client.request( + "post", + "/v3/kyb/search/", + cast_to=KybSearchResponse, + auth="api_key", + body={ + "country_code": country_code, + "name": name, + "registration_number": registration_number, + "search_type": search_type, + "vendor_data": vendor_data, + "metadata": metadata, + "webhook_url": webhook_url, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def select( + self, + *, + kyb_response_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybSelectResponse: + """KYB registry company select - Lite tier (standalone). + + Retrieve the **Lite** registry profile of one candidate returned by `POST /v3/kyb/search/` - company details, registered address, identifiers, status, activity and the officers, plus the raw registry payload. Shareholders and ultimate beneficial owners are sold by `POST /v3/kyb/shareholders/` and `POST /v3/kyb/ubo/`. + + **Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The handle is ephemeral and scoped to that search - select promptly, and re-search for a fresh handle rather than storing it. + + **Billing.** Each select is billed at the country's Lite price (any search fee is separate); see [KYB registry pricing](/getting-started/kyb-registry-pricing). The balance is checked before any registry call (`403` when short) and the charge is recorded against the created business session. + + **Persistence is mandatory.** Every select creates a business session (its id is the `request_id`) so billing and retrieval can be tracked; it appears in the console. `save_api_request` is accepted for symmetry but must be `true` (the default) - sending `false` returns `400`. + + ``POST /v3/kyb/select/`` + + Args: + kyb_response_id: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it. + vendor_data: Your identifier for this company; echoed back and stored with the business session. + metadata: Free-form JSON stored with the business session and echoed back. + save_api_request: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybSelectResponse", + await self._client.request( + "post", + "/v3/kyb/select/", + cast_to=KybSelectResponse, + auth="api_key", + body={ + "kyb_response_id": kyb_response_id, + "vendor_data": vendor_data, + "metadata": metadata, + "save_api_request": save_api_request, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_shareholders( + self, + *, + kyb_response_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybListShareholdersResponse | KybListShareholdersResponse202: + """KYB registry company with shareholders (standalone). + + Retrieve the Lite profile **plus the direct shareholders** (natural persons and corporate entities, with their ownership bands) of one candidate returned by `POST /v3/kyb/search/`. + + **Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost. + + **Billing.** Billed at the country's Shareholders price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). When the registry holds no shareholder record for the company (or the country offers no ownership data at all) the Lite profile is delivered and billed instead: read `registry_tier_delivered` and `registry_ownership_status`. The balance is checked against the Shareholders price before any registry call (`403` when short). + + **Availability.** v3 only. Every call creates a business session (its id is the `request_id`). + + **Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request. + + ``POST /v3/kyb/shareholders/`` + + Args: + kyb_response_id: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it. + vendor_data: Your identifier for this company; echoed back and stored with the business session. + metadata: Free-form JSON stored with the business session and echoed back. + save_api_request: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybListShareholdersResponse | KybListShareholdersResponse202", + await self._client.request( + "post", + "/v3/kyb/shareholders/", + cast_to={200: KybListShareholdersResponse, 202: KybListShareholdersResponse202}, + auth="api_key", + body={ + "kyb_response_id": kyb_response_id, + "vendor_data": vendor_data, + "metadata": metadata, + "save_api_request": save_api_request, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_ubos( + self, + *, + kyb_response_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> KybListUbosResponse | KybListUbosResponse202: + """KYB registry company with ultimate beneficial owners (standalone). + + Retrieve the Lite profile, the direct shareholders **and the ultimate beneficial owners** resolved through the ownership chain (`registry_data.ubo_resolution` carries the corporate parent chain) of one candidate returned by `POST /v3/kyb/search/`. + + **Pairing with search.** Pass the candidate's `kyb_response_id` exactly as returned by a search of the same application. The search runs on the Lite registry chain; when the country's ownership data lives at another registry Didit re-finds the company there by registration number, at no extra cost. + + **Billing.** Billed at the country's UBO price - see [KYB registry pricing](/getting-started/kyb-registry-pricing). The tier delivered is the highest the country and the registry can serve: Shareholders when the resolved report names no natural person as a beneficial owner, Lite when there is no ownership record at all; `registry_tier_delivered` names what was billed. The balance is checked against the UBO price before any registry call (`403` when short). + + **Availability.** v3 only. Every call creates a business session (its id is the `request_id`). + + **Pending reports.** When a report is still being prepared, or the ownership section has not been retrieved from the registry yet, the endpoint returns 202 Accepted, kyb_registry.fetch_status=pending, kyb_registry.registry_ownership_status=pending, and registry_tier_delivered=null. Keep request_id and retrieve GET /v3/session/{request_id}/decision/ until registry_checks[0].company.fetch_status is resolved. Didit completes the report automatically and charges the delivered tier once it is ready. Do not repeat the purchase while it is pending; every new POST creates a separate request. + + ``POST /v3/kyb/ubo/`` + + Args: + kyb_response_id: Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it. + vendor_data: Your identifier for this company; echoed back and stored with the business session. + metadata: Free-form JSON stored with the business session and echoed back. + save_api_request: Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "KybListUbosResponse | KybListUbosResponse202", + await self._client.request( + "post", + "/v3/kyb/ubo/", + cast_to={200: KybListUbosResponse, 202: KybListUbosResponse202}, + auth="api_key", + body={ + "kyb_response_id": kyb_response_id, + "vendor_data": vendor_data, + "metadata": metadata, + "save_api_request": save_api_request, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class KybWithRawResponse: + def __init__(self, resource: Kyb) -> None: + self._resource = resource + self.search = to_raw_response_wrapper(resource.search) + self.select = to_raw_response_wrapper(resource.select) + self.list_shareholders = to_raw_response_wrapper(resource.list_shareholders) + self.list_ubos = to_raw_response_wrapper(resource.list_ubos) + + +class AsyncKybWithRawResponse: + def __init__(self, resource: AsyncKyb) -> None: + self._resource = resource + self.search = async_to_raw_response_wrapper(resource.search) + self.select = async_to_raw_response_wrapper(resource.select) + self.list_shareholders = async_to_raw_response_wrapper(resource.list_shareholders) + self.list_ubos = async_to_raw_response_wrapper(resource.list_ubos) + + +class KybWithStreamingResponse: + def __init__(self, resource: Kyb) -> None: + self._resource = resource + self.search = to_streamed_response_wrapper(resource.search) + self.select = to_streamed_response_wrapper(resource.select) + self.list_shareholders = to_streamed_response_wrapper(resource.list_shareholders) + self.list_ubos = to_streamed_response_wrapper(resource.list_ubos) + + +class AsyncKybWithStreamingResponse: + def __init__(self, resource: AsyncKyb) -> None: + self._resource = resource + self.search = async_to_streamed_response_wrapper(resource.search) + self.select = async_to_streamed_response_wrapper(resource.select) + self.list_shareholders = async_to_streamed_response_wrapper(resource.list_shareholders) + self.list_ubos = async_to_streamed_response_wrapper(resource.list_ubos) diff --git a/src/didit/resources/networks/__init__.py b/src/didit/resources/networks/__init__.py new file mode 100644 index 0000000..254c156 --- /dev/null +++ b/src/didit/resources/networks/__init__.py @@ -0,0 +1,873 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``networks`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import TYPE_CHECKING, Literal, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.network_detail import NetworkDetail +from didit.types.components.network_graph import NetworkGraph +from didit.types.components.network_list_item import NetworkListItem +from didit.types.components.network_map import NetworkMap +from didit.types.networks.list_members_response import NetworksListMembersResponse +from didit.types.networks.list_response import NetworksListResponse +from didit.types.networks.list_signals_response import NetworksListSignalsResponse +from didit.types.networks.retrieve_timeline_response import NetworksRetrieveTimelineResponse + +if TYPE_CHECKING: + from didit.resources.networks.notes import ( + AsyncNetworksNotes, + AsyncNetworksNotesWithRawResponse, + AsyncNetworksNotesWithStreamingResponse, + NetworksNotes, + NetworksNotesWithRawResponse, + NetworksNotesWithStreamingResponse, + ) + +__all__ = [ + "Networks", + "AsyncNetworks", + "NetworksWithRawResponse", + "AsyncNetworksWithRawResponse", + "NetworksWithStreamingResponse", + "AsyncNetworksWithStreamingResponse", +] + + +class Networks(SyncAPIResource): + """The ``networks`` resource of the Didit API. + + Methods: ``download_report``, ``list``, ``list_members``, ``list_signals``, ``retrieve``, ``retrieve_graph``, ``retrieve_map``, ``retrieve_timeline``. + + Nested resources: ``notes``. + """ + + @cached_property + def notes(self) -> NetworksNotes: + from didit.resources.networks.notes import NetworksNotes + + return NetworksNotes(self._client) + + @cached_property + def with_raw_response(self) -> NetworksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return NetworksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> NetworksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return NetworksWithStreamingResponse(self) + + def list( + self, + *, + status: str | NotGiven = NOT_GIVEN, + signal_type: str | NotGiven = NOT_GIVEN, + pattern_type: str | NotGiven = NOT_GIVEN, + risk_band: str | NotGiven = NOT_GIVEN, + min_size: int | NotGiven = NOT_GIVEN, + max_size: int | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + q: str | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[NetworkListItem]: + """List networks. + + List fraud networks detected within this application's organization, newest activity first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks - clustering excludes sandbox data entirely. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/`` + + Args: + status: Comma-separated statuses to include: active,in_review,resolved,dismissed. Omit to exclude dismissed. + signal_type: Comma-separated signal types: ip_address,device,face,document_number,phone,email,address. + pattern_type: Comma-separated pattern types, e.g. exact_same_device,same_email. + risk_band: Comma-separated bands: low,medium,high. + min_size: Minimum member count. + max_size: Maximum member count. + date_from: Lower bound on last_activity_at (ISO 8601 date or datetime). + date_to: Upper bound on first_activity_at (ISO 8601 date or datetime). + q: Free-text search across network name, member names, the network_id (N-0001), and exact pattern_type tokens. + ordering: One of risk_score, -risk_score, last_activity_at, -last_activity_at, first_activity_at, -first_activity_at, network_number, -network_number. Defaults to -risk_score,-last_activity_at. + limit: Page size, 1-500. Defaults to 100. + offset: Zero-based offset of the first record. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[NetworkListItem]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/", + cast_to=NetworksListResponse, + item_type=NetworkListItem, + scope="application", + auth="api_key", + query={ + "status": status, + "signal_type": signal_type, + "pattern_type": pattern_type, + "risk_band": risk_band, + "min_size": min_size, + "max_size": max_size, + "date_from": date_from, + "date_to": date_to, + "q": q, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkDetail: + """Get network. + + Retrieve a single network's full detail, including stats, fraud tags, and risk signals. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkDetail", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/", + cast_to=NetworkDetail, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_graph( + self, + network_uuid: str, + *, + depth: Literal["1", "2", "3", "all"] | str | NotGiven = NOT_GIVEN, + focus_kind: Literal["vendor_user", "vendor_business", "transaction_party"] + | str + | NotGiven = NOT_GIVEN, + focus_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkGraph: + """Get network graph. + + Retrieve the network as a graph of subject nodes (vendor users, vendor businesses, transaction parties) and signal nodes (the shared device/IP/face/document/phone/email/address values linking them), with edges representing memberships. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/`` + + Args: + network_uuid: Network UUID. + depth: Hops from the focus subject to include. Defaults to all (unbounded). + focus_kind: Subject type to center the graph on. Must be supplied together with focus_id; defaults to an arbitrary member when depth is bounded and neither is supplied. + focus_id: Subject UUID to center the graph on. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkGraph", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/", + cast_to=NetworkGraph, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + query={"depth": depth, "focus_kind": focus_kind, "focus_id": focus_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_members( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworksListMembersResponse: + """List network members. + + List the subjects (vendor users, vendor businesses, or transaction parties) that belong to this network. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworksListMembersResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/", + cast_to=NetworksListMembersResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_signals( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworksListSignalsResponse: + """List network signals. + + List the signals (shared device, IP, face, document number, phone, email, or address values) that link this network's members. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworksListSignalsResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/", + cast_to=NetworksListSignalsResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_timeline( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworksRetrieveTimelineResponse: + """Get network timeline. + + Chronological events for this network: detection, each signal's observation, and lifecycle status changes. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworksRetrieveTimelineResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/", + cast_to=NetworksRetrieveTimelineResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_map( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkMap: + """Get network map. + + Geographic points where this network's members' addresses/locations converge, sourced from the sessions, business sessions, and transactions backing its memberships. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkMap", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/", + cast_to=NetworkMap, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def download_report( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a network evidence report. + + Download a PDF of the current organization network, its observed account connections, signal references and review status. Requires read:networks. Global intelligence and private analyst notes are not included. Each account and signal list is capped at 500 entries and the report states when it is incomplete. The request does not change network status. Errors are returned as JSON. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/", + cast_to=bytes, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncNetworks(AsyncAPIResource): + """The ``networks`` resource of the Didit API. + + Methods: ``download_report``, ``list``, ``list_members``, ``list_signals``, ``retrieve``, ``retrieve_graph``, ``retrieve_map``, ``retrieve_timeline``. + + Nested resources: ``notes``. + """ + + @cached_property + def notes(self) -> AsyncNetworksNotes: + from didit.resources.networks.notes import AsyncNetworksNotes + + return AsyncNetworksNotes(self._client) + + @cached_property + def with_raw_response(self) -> AsyncNetworksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncNetworksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncNetworksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncNetworksWithStreamingResponse(self) + + def list( + self, + *, + status: str | NotGiven = NOT_GIVEN, + signal_type: str | NotGiven = NOT_GIVEN, + pattern_type: str | NotGiven = NOT_GIVEN, + risk_band: str | NotGiven = NOT_GIVEN, + min_size: int | NotGiven = NOT_GIVEN, + max_size: int | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + q: str | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[NetworkListItem]: + """List networks. + + List fraud networks detected within this application's organization, newest activity first by default. By default dismissed networks are excluded; pass status to include them. Sandbox applications never have networks - clustering excludes sandbox data entirely. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/`` + + Args: + status: Comma-separated statuses to include: active,in_review,resolved,dismissed. Omit to exclude dismissed. + signal_type: Comma-separated signal types: ip_address,device,face,document_number,phone,email,address. + pattern_type: Comma-separated pattern types, e.g. exact_same_device,same_email. + risk_band: Comma-separated bands: low,medium,high. + min_size: Minimum member count. + max_size: Maximum member count. + date_from: Lower bound on last_activity_at (ISO 8601 date or datetime). + date_to: Upper bound on first_activity_at (ISO 8601 date or datetime). + q: Free-text search across network name, member names, the network_id (N-0001), and exact pattern_type tokens. + ordering: One of risk_score, -risk_score, last_activity_at, -last_activity_at, first_activity_at, -first_activity_at, network_number, -network_number. Defaults to -risk_score,-last_activity_at. + limit: Page size, 1-500. Defaults to 100. + offset: Zero-based offset of the first record. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[NetworkListItem]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/", + cast_to=NetworksListResponse, + item_type=NetworkListItem, + scope="application", + auth="api_key", + query={ + "status": status, + "signal_type": signal_type, + "pattern_type": pattern_type, + "risk_band": risk_band, + "min_size": min_size, + "max_size": max_size, + "date_from": date_from, + "date_to": date_to, + "q": q, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkDetail: + """Get network. + + Retrieve a single network's full detail, including stats, fraud tags, and risk signals. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkDetail", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/", + cast_to=NetworkDetail, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_graph( + self, + network_uuid: str, + *, + depth: Literal["1", "2", "3", "all"] | str | NotGiven = NOT_GIVEN, + focus_kind: Literal["vendor_user", "vendor_business", "transaction_party"] + | str + | NotGiven = NOT_GIVEN, + focus_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkGraph: + """Get network graph. + + Retrieve the network as a graph of subject nodes (vendor users, vendor businesses, transaction parties) and signal nodes (the shared device/IP/face/document/phone/email/address values linking them), with edges representing memberships. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/`` + + Args: + network_uuid: Network UUID. + depth: Hops from the focus subject to include. Defaults to all (unbounded). + focus_kind: Subject type to center the graph on. Must be supplied together with focus_id; defaults to an arbitrary member when depth is bounded and neither is supplied. + focus_id: Subject UUID to center the graph on. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkGraph", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/", + cast_to=NetworkGraph, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + query={"depth": depth, "focus_kind": focus_kind, "focus_id": focus_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_members( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworksListMembersResponse: + """List network members. + + List the subjects (vendor users, vendor businesses, or transaction parties) that belong to this network. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworksListMembersResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/", + cast_to=NetworksListMembersResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_signals( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworksListSignalsResponse: + """List network signals. + + List the signals (shared device, IP, face, document number, phone, email, or address values) that link this network's members. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworksListSignalsResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/", + cast_to=NetworksListSignalsResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_timeline( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworksRetrieveTimelineResponse: + """Get network timeline. + + Chronological events for this network: detection, each signal's observation, and lifecycle status changes. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworksRetrieveTimelineResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/", + cast_to=NetworksRetrieveTimelineResponse, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_map( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkMap: + """Get network map. + + Geographic points where this network's members' addresses/locations converge, sourced from the sessions, business sessions, and transactions backing its memberships. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkMap", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/", + cast_to=NetworkMap, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def download_report( + self, + network_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a network evidence report. + + Download a PDF of the current organization network, its observed account connections, signal references and review status. Requires read:networks. Global intelligence and private analyst notes are not included. Each account and signal list is capped at 500 entries and the report states when it is incomplete. The request does not change network status. Errors are returned as JSON. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/`` + + Args: + network_uuid: Network UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/", + cast_to=bytes, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class NetworksWithRawResponse: + def __init__(self, resource: Networks) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.retrieve_graph = to_raw_response_wrapper(resource.retrieve_graph) + self.list_members = to_raw_response_wrapper(resource.list_members) + self.list_signals = to_raw_response_wrapper(resource.list_signals) + self.retrieve_timeline = to_raw_response_wrapper(resource.retrieve_timeline) + self.retrieve_map = to_raw_response_wrapper(resource.retrieve_map) + self.download_report = to_raw_response_wrapper(resource.download_report) + + @cached_property + def notes(self) -> NetworksNotesWithRawResponse: + from didit.resources.networks.notes import NetworksNotesWithRawResponse + + return NetworksNotesWithRawResponse(self._resource.notes) + + +class AsyncNetworksWithRawResponse: + def __init__(self, resource: AsyncNetworks) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.retrieve_graph = async_to_raw_response_wrapper(resource.retrieve_graph) + self.list_members = async_to_raw_response_wrapper(resource.list_members) + self.list_signals = async_to_raw_response_wrapper(resource.list_signals) + self.retrieve_timeline = async_to_raw_response_wrapper(resource.retrieve_timeline) + self.retrieve_map = async_to_raw_response_wrapper(resource.retrieve_map) + self.download_report = async_to_raw_response_wrapper(resource.download_report) + + @cached_property + def notes(self) -> AsyncNetworksNotesWithRawResponse: + from didit.resources.networks.notes import AsyncNetworksNotesWithRawResponse + + return AsyncNetworksNotesWithRawResponse(self._resource.notes) + + +class NetworksWithStreamingResponse: + def __init__(self, resource: Networks) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.retrieve_graph = to_streamed_response_wrapper(resource.retrieve_graph) + self.list_members = to_streamed_response_wrapper(resource.list_members) + self.list_signals = to_streamed_response_wrapper(resource.list_signals) + self.retrieve_timeline = to_streamed_response_wrapper(resource.retrieve_timeline) + self.retrieve_map = to_streamed_response_wrapper(resource.retrieve_map) + self.download_report = to_streamed_response_wrapper(resource.download_report) + + @cached_property + def notes(self) -> NetworksNotesWithStreamingResponse: + from didit.resources.networks.notes import NetworksNotesWithStreamingResponse + + return NetworksNotesWithStreamingResponse(self._resource.notes) + + +class AsyncNetworksWithStreamingResponse: + def __init__(self, resource: AsyncNetworks) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.retrieve_graph = async_to_streamed_response_wrapper(resource.retrieve_graph) + self.list_members = async_to_streamed_response_wrapper(resource.list_members) + self.list_signals = async_to_streamed_response_wrapper(resource.list_signals) + self.retrieve_timeline = async_to_streamed_response_wrapper(resource.retrieve_timeline) + self.retrieve_map = async_to_streamed_response_wrapper(resource.retrieve_map) + self.download_report = async_to_streamed_response_wrapper(resource.download_report) + + @cached_property + def notes(self) -> AsyncNetworksNotesWithStreamingResponse: + from didit.resources.networks.notes import AsyncNetworksNotesWithStreamingResponse + + return AsyncNetworksNotesWithStreamingResponse(self._resource.notes) diff --git a/src/didit/resources/networks/notes.py b/src/didit/resources/networks/notes.py new file mode 100644 index 0000000..c6a5d58 --- /dev/null +++ b/src/didit/resources/networks/notes.py @@ -0,0 +1,350 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``networks.notes`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.network_note import NetworkNote +from didit.types.networks.notes.list_response import NetworksNotesListResponse + +__all__ = [ + "NetworksNotes", + "AsyncNetworksNotes", + "NetworksNotesWithRawResponse", + "AsyncNetworksNotesWithRawResponse", + "NetworksNotesWithStreamingResponse", + "AsyncNetworksNotesWithStreamingResponse", +] + + +class NetworksNotes(SyncAPIResource): + """The ``networks.notes`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> NetworksNotesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return NetworksNotesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> NetworksNotesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return NetworksNotesWithStreamingResponse(self) + + def delete( + self, + network_uuid: str, + note_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete your network note. + + Requires write:networks and the authenticated author of the note. Deletion is irreversible. Connected account and verification data are unaffected. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/`` + + Args: + network_uuid: Network UUID. + note_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/", + cast_to=None, + path_params={"network_uuid": network_uuid, "note_uuid": note_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + network_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[NetworkNote]: + """List private network notes. + + Console users with read:networks can read notes on a network visible to the application. Notes stay within the organization and are not contributed to shared intelligence. Newest notes appear first. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/`` + + Args: + network_uuid: Network UUID. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[NetworkNote]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/", + cast_to=NetworksNotesListResponse, + item_type=NetworkNote, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + network_uuid: str, + *, + comment: str, + client_reference: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkNote: + """Add a private network note. + + Requires a console user with write:networks. The author is assigned from the authenticated user. Supply a new random UUID client_reference for each note; retry the same body and reference after an uncertain response to avoid duplicates. Reusing a reference for another body, author or network returns 409. Notes follow the surviving investigation when networks merge. + + ``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/`` + + Args: + network_uuid: Network UUID. + comment + client_reference + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkNote", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/", + cast_to=NetworkNote, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + body={"comment": comment, "client_reference": client_reference}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncNetworksNotes(AsyncAPIResource): + """The ``networks.notes`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncNetworksNotesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncNetworksNotesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncNetworksNotesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncNetworksNotesWithStreamingResponse(self) + + async def delete( + self, + network_uuid: str, + note_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete your network note. + + Requires write:networks and the authenticated author of the note. Deletion is irreversible. Connected account and verification data are unaffected. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/`` + + Args: + network_uuid: Network UUID. + note_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/", + cast_to=None, + path_params={"network_uuid": network_uuid, "note_uuid": note_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + network_uuid: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[NetworkNote]: + """List private network notes. + + Console users with read:networks can read notes on a network visible to the application. Notes stay within the organization and are not contributed to shared intelligence. Newest notes appear first. + + ``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/`` + + Args: + network_uuid: Network UUID. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[NetworkNote]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/", + cast_to=NetworksNotesListResponse, + item_type=NetworkNote, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + network_uuid: str, + *, + comment: str, + client_reference: str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> NetworkNote: + """Add a private network note. + + Requires a console user with write:networks. The author is assigned from the authenticated user. Supply a new random UUID client_reference for each note; retry the same body and reference after an uncertain response to avoid duplicates. Reusing a reference for another body, author or network returns 409. Notes follow the surviving investigation when networks merge. + + ``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/`` + + Args: + network_uuid: Network UUID. + comment + client_reference + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "NetworkNote", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/", + cast_to=NetworkNote, + path_params={"network_uuid": network_uuid}, + scope="application", + auth="api_key", + body={"comment": comment, "client_reference": client_reference}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class NetworksNotesWithRawResponse: + def __init__(self, resource: NetworksNotes) -> None: + self._resource = resource + self.delete = to_raw_response_wrapper(resource.delete) + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + + +class AsyncNetworksNotesWithRawResponse: + def __init__(self, resource: AsyncNetworksNotes) -> None: + self._resource = resource + self.delete = async_to_raw_response_wrapper(resource.delete) + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + + +class NetworksNotesWithStreamingResponse: + def __init__(self, resource: NetworksNotes) -> None: + self._resource = resource + self.delete = to_streamed_response_wrapper(resource.delete) + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + + +class AsyncNetworksNotesWithStreamingResponse: + def __init__(self, resource: AsyncNetworksNotes) -> None: + self._resource = resource + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) diff --git a/src/didit/resources/passive_liveness.py b/src/didit/resources/passive_liveness.py new file mode 100644 index 0000000..d605499 --- /dev/null +++ b/src/didit/resources/passive_liveness.py @@ -0,0 +1,238 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``passive_liveness`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.passive_liveness.check_response import PassiveLivenessCheckResponse + +__all__ = [ + "PassiveLiveness", + "AsyncPassiveLiveness", + "PassiveLivenessWithRawResponse", + "AsyncPassiveLivenessWithRawResponse", + "PassiveLivenessWithStreamingResponse", + "AsyncPassiveLivenessWithStreamingResponse", +] + + +class PassiveLiveness(SyncAPIResource): + """The ``passive_liveness`` resource of the Didit API. + + Methods: ``check``. + """ + + @cached_property + def with_raw_response(self) -> PassiveLivenessWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return PassiveLivenessWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PassiveLivenessWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return PassiveLivenessWithStreamingResponse(self) + + def check( + self, + *, + user_image: FileTypes, + face_liveness_score_decline_threshold: float | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PassiveLivenessCheckResponse: + """Passive Liveness. + + Detect presentation attacks - printed photos, screen replays, masks, deepfakes - from a single face image. No video, motion, or user interaction required. + + **How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires: + - `NO_FACE_DETECTED` - the liveness model found no face, + - `LOW_LIVENESS_SCORE` - score at or below `face_liveness_score_decline_threshold` (default `30`), + - `LIVENESS_FACE_ATTACK` - the model detected a presentation attack, + - `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` - the face matches an entry on your face blocklist. + + `MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality. + + **Blocklist and duplicate screening.** Runs on **every** call - with or without `save_api_request` - by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search - so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces). + + **Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. + + **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs). + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/passive-liveness/`` + + Args: + user_image: Front-facing face image (e.g. a selfie). Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image should contain a single subject - when several faces are present, the largest one is evaluated and a `MULTIPLE_FACES_DETECTED` warning is added. + face_liveness_score_decline_threshold: Liveness score threshold (0–100). A computed score **at or below** this value emits a `LOW_LIVENESS_SCORE` warning and sets `status` to `Declined`. Default `30`; raise it for a stricter anti-spoofing posture. Values outside 0–100 return `400`. + rotate_image: When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency. + save_api_request: When `true` (default), persists the call as an API-type session, stores the face images, emits a `status.updated` webhook, and enrolls the detected face into your face search index. When `false`, the call is one-shot - nothing is stored and the face is not enrolled. Blocklist and duplicate screening run either way. + vendor_data: Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. Also used to exclude prior sessions with the same `vendor_data` from the duplicate-face check, so re-verifying the same user does not raise `DUPLICATED_FACE`. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PassiveLivenessCheckResponse", + self._client.request( + "post", + "/v3/passive-liveness/", + cast_to=PassiveLivenessCheckResponse, + auth="api_key", + body={ + "user_image": user_image, + "face_liveness_score_decline_threshold": face_liveness_score_decline_threshold, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("user_image",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncPassiveLiveness(AsyncAPIResource): + """The ``passive_liveness`` resource of the Didit API. + + Methods: ``check``. + """ + + @cached_property + def with_raw_response(self) -> AsyncPassiveLivenessWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncPassiveLivenessWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPassiveLivenessWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncPassiveLivenessWithStreamingResponse(self) + + async def check( + self, + *, + user_image: FileTypes, + face_liveness_score_decline_threshold: float | NotGiven = NOT_GIVEN, + rotate_image: bool | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PassiveLivenessCheckResponse: + """Passive Liveness. + + Detect presentation attacks - printed photos, screen replays, masks, deepfakes - from a single face image. No video, motion, or user interaction required. + + **How it works.** The image is analyzed by a biometric model; the largest detected face is evaluated and a liveness `score` (0–100) is computed. `status` is `Declined` when any `error`-severity warning fires: + - `NO_FACE_DETECTED` - the liveness model found no face, + - `LOW_LIVENESS_SCORE` - score at or below `face_liveness_score_decline_threshold` (default `30`), + - `LIVENESS_FACE_ATTACK` - the model detected a presentation attack, + - `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` - the face matches an entry on your face blocklist. + + `MULTIPLE_FACES_DETECTED` is a `warning`-severity signal (does not decline), and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` are `information`-severity signals that the same face already appeared in another approved session (they never decline). `face_quality` and `face_luminance` (0–100, nullable) describe capture quality. + + **Blocklist and duplicate screening.** Runs on **every** call - with or without `save_api_request` - by matching the detected face against your application's face search index. Prior faces with the same `vendor_data` are excluded from the entire index search - so re-running liveness for the same user does not trigger `DUPLICATED_FACE`, and a blocklisted face originating from a session with the caller's own `vendor_data` is also NOT flagged (`FACE_IN_BLOCKLIST` screening is bypassed for same-`vendor_data` faces). + + **Billing.** Each `200` response consumes one Passive Liveness API credit (standalone APIs have no free tier). Insufficient balance returns `403` before any image processing. + + **Session persistence and face enrollment (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session (Business Console, `GET /v3/session/{sessionId}/decision/` via the returned `request_id`, `status.updated` webhook), the reference image and face crop are stored, and the detected face is **enrolled into your face search index** so later Face Search calls and duplicate checks can match it. When `false`, the call is one-shot: nothing is stored and the face is not enrolled (blocklist/duplicate screening still runs). + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload, no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + ``POST /v3/passive-liveness/`` + + Args: + user_image: Front-facing face image (e.g. a selfie). Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image should contain a single subject - when several faces are present, the largest one is evaluated and a `MULTIPLE_FACES_DETECTED` warning is added. + face_liveness_score_decline_threshold: Liveness score threshold (0–100). A computed score **at or below** this value emits a `LOW_LIVENESS_SCORE` warning and sets `status` to `Declined`. Default `30`; raise it for a stricter anti-spoofing posture. Values outside 0–100 return `400`. + rotate_image: When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency. + save_api_request: When `true` (default), persists the call as an API-type session, stores the face images, emits a `status.updated` webhook, and enrolls the detected face into your face search index. When `false`, the call is one-shot - nothing is stored and the face is not enrolled. Blocklist and duplicate screening run either way. + vendor_data: Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. Also used to exclude prior sessions with the same `vendor_data` from the duplicate-face check, so re-verifying the same user does not raise `DUPLICATED_FACE`. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PassiveLivenessCheckResponse", + await self._client.request( + "post", + "/v3/passive-liveness/", + cast_to=PassiveLivenessCheckResponse, + auth="api_key", + body={ + "user_image": user_image, + "face_liveness_score_decline_threshold": face_liveness_score_decline_threshold, + "rotate_image": rotate_image, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("user_image",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class PassiveLivenessWithRawResponse: + def __init__(self, resource: PassiveLiveness) -> None: + self._resource = resource + self.check = to_raw_response_wrapper(resource.check) + + +class AsyncPassiveLivenessWithRawResponse: + def __init__(self, resource: AsyncPassiveLiveness) -> None: + self._resource = resource + self.check = async_to_raw_response_wrapper(resource.check) + + +class PassiveLivenessWithStreamingResponse: + def __init__(self, resource: PassiveLiveness) -> None: + self._resource = resource + self.check = to_streamed_response_wrapper(resource.check) + + +class AsyncPassiveLivenessWithStreamingResponse: + def __init__(self, resource: AsyncPassiveLiveness) -> None: + self._resource = resource + self.check = async_to_streamed_response_wrapper(resource.check) diff --git a/src/didit/resources/phone.py b/src/didit/resources/phone.py new file mode 100644 index 0000000..d6c2bc6 --- /dev/null +++ b/src/didit/resources/phone.py @@ -0,0 +1,537 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``phone`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.phone_risk_response import PhoneRiskResponse +from didit.types.components.phone_verification_check_response import PhoneVerificationCheckResponse +from didit.types.components.phone_verification_send_response import PhoneVerificationSendResponse +from didit.types.components.risk_signals import RiskSignals +from didit.types.phone.send_code_params import ( + PhoneSendCodeParamsOptions, + PhoneSendCodeParamsSignals, +) + +__all__ = [ + "Phone", + "AsyncPhone", + "PhoneWithRawResponse", + "AsyncPhoneWithRawResponse", + "PhoneWithStreamingResponse", + "AsyncPhoneWithStreamingResponse", +] + + +class Phone(SyncAPIResource): + """The ``phone`` resource of the Didit API. + + Methods: ``assess_risk``, ``check_code``, ``send_code``. + """ + + @cached_property + def with_raw_response(self) -> PhoneWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return PhoneWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> PhoneWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return PhoneWithStreamingResponse(self) + + def assess_risk( + self, + *, + phone_number: str, + country_code: str | NotGiven = NOT_GIVEN, + signals: RiskSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PhoneRiskResponse: + """Risk-score a phone number. + + Resolve a phone number and return carrier, line-type, porting, disposable-number, cross-session match, and normalized risk-intelligence signals without sending an OTP. The endpoint creates an API session and returns its id as `request_id`. + + **Decision.** The top-level `status` mirrors `phone.status`. It is normally `Approved`; the risk evaluation can produce `In Review` or `Declined`. + + **Compatibility aliases.** Live responses expose the normalized intelligence object as both `phone.phone_intelligence` and `phone.enrichment`. The values are identical. New integrations can read `phone.enrichment`. Both keys are absent when no usable intelligence data was returned. + + **Billing.** Live calls use Phone Risk API pricing. The request is billable once the lookup starts, even when some best-effort fields are unavailable. + + **Sandbox.** Sandbox keys validate the request and return deterministic approved data without sending an OTP, consuming credits, creating a session, or contributing to Networks. + + **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + + **Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price. + + ``POST /v3/phone/risk/`` + + Args: + phone_number: Phone number to score. It is validated and normalized to E.164. + country_code: Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage. + signals: Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks. + vendor_data: Your stable reference for this request. Stored on the API session and echoed in the response. + metadata: Your non-sensitive JSON metadata. Stored on the API session and echoed in the response. + enable_social: When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PhoneRiskResponse", + self._client.request( + "post", + "/v3/phone/risk/", + cast_to=PhoneRiskResponse, + auth="api_key", + body={ + "phone_number": phone_number, + "country_code": country_code, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + "enable_social": enable_social, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def send_code( + self, + *, + phone_number: str, + options: PhoneSendCodeParamsOptions | NotGiven = NOT_GIVEN, + signals: PhoneSendCodeParamsSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PhoneVerificationSendResponse: + """Send Phone Code. + + Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or another supported channel, then verify it with [`POST /v3/phone/check/`](/standalone-apis/phone-check). + + **How send and check pair up.** Verification state is keyed by your application plus the E.164 `phone_number` - the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same number and the code the user received before the window closes. Calling send again for the same number while a verification is pending re-sends a code for that same verification (`status: "Retry"`, same `request_id`). At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries. + + **Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS - the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events). + + **Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message and are not branded. + + **Send statuses.** `Success` - OTP sent for a new verification; `Retry` - OTP re-sent for the pending verification; `Blocked` - the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`. + + **Billing.** The price depends on the destination country and the channel that actually delivers the message - see [Phone Verification Pricing](/getting-started/phone-verification-pricing). Only `Blocked` sends are guaranteed free: any other unbilled send attempt is charged at verification finalization or expiration, even if delivery was never confirmed or the delivery provider reported the message undeliverable. Checks are always free. As an anti-abuse measure, phone verification is disabled until your organization's first top-up (`403`). + + **Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses. + + **Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no message is sent and nothing is persisted. Use code `123456` on the sandbox check. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limits.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send attempts per phone number per hour; exceeding either returns `429`. + + ``POST /v3/phone/send/`` + + Args: + phone_number: Recipient phone number in E.164 format - leading `+` and country code (e.g., `+14155552671`). Numbers are validated and normalized to E.164; unparseable or invalid numbers return `400` with a `phone_number` field error. + options: OTP delivery options. All fields are optional. + signals: Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional. + vendor_data: Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record. + metadata: Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PhoneVerificationSendResponse", + self._client.request( + "post", + "/v3/phone/send/", + cast_to=PhoneVerificationSendResponse, + auth="api_key", + body={ + "phone_number": phone_number, + "options": options, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def check_code( + self, + *, + phone_number: str, + code: str, + duplicated_phone_number_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + disposable_number_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + voip_number_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + phone_no_social_presence_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PhoneVerificationCheckResponse: + """Check Phone Code. + + Verify the OTP delivered by [`POST /v3/phone/send/`](/standalone-apis/phone-send) and get the final verification result plus phone intelligence: carrier name and line type, virtual (VoIP) and disposable flags, and duplicate usage of the number across your sessions. + + **How the check finds the verification.** Matching is by your application plus the E.164 `phone_number` - `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, or older than 5 minutes) the endpoint returns `200` with `status: "Expired or Not Found"`. + + **Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: "Failed"` with the attempts remaining and `phone: null`; the third wrong code finalizes the verification as `Declined` with a `VERIFICATION_CODE_ATTEMPTS_EXCEEDED` warning and returns the full phone report. + + **Outcomes.** `Approved` - correct code and no declining risk. `Declined` - terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted or high-risk number), or the attempt budget was exhausted. `Failed` - wrong code, attempts remaining. `Expired or Not Found` - nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `phone` carries the full report (`carrier`, `is_virtual`, `is_disposable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID. + + **Risk actions.** `duplicated_phone_number_action`, `disposable_number_action`, and `voip_number_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `phone.warnings` without affecting the status. + + **Billing.** Checks are free - delivery is billed on the send side (see [Phone Verification Pricing](/getting-started/phone-verification-pricing)). + + **Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook. + + **Sandbox.** Sandbox API keys skip all processing: any well-formed code (4–8 digits, `123456` included) returns a static `Approved` payload with a simplified flat `phone` object (`status`, `phone_number`, `country_code`, `carrier_name`, `line_type`, `flags`); malformed input still returns `400`. Nothing is persisted. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limits.** Shared write budget of 300 requests/min per API key, plus the upstream anti-abuse cap of 4 attempts per number per hour; exceeding either returns `429`. + + **Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price. + + ``POST /v3/phone/check/`` + + Args: + phone_number: The same phone number used in the matching [`POST /v3/phone/send/`](/standalone-apis/phone-send) call, in E.164 format. This is what links the check to the send. + code: The OTP the end user received. Must be 4–8 numeric digits - anything else returns `400` without consuming an attempt. A well-formed but wrong code returns `200` with `status: "Failed"` and consumes one of the 3 attempts. + duplicated_phone_number_action: What to do when the same number was already used by a different user (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings` and fills `phone.matches`. + disposable_number_action: What to do when the carrier lookup flags the number as a temporary/burner line (`phone.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`. + voip_number_action: What to do when the carrier lookup reports a virtual line type - `voip`, `isp`, or `vpn` (`phone.is_virtual`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`. + enable_social: When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price. + phone_no_social_presence_action: Action when the social footprint check finds the number registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PhoneVerificationCheckResponse", + self._client.request( + "post", + "/v3/phone/check/", + cast_to=PhoneVerificationCheckResponse, + auth="api_key", + body={ + "phone_number": phone_number, + "code": code, + "duplicated_phone_number_action": duplicated_phone_number_action, + "disposable_number_action": disposable_number_action, + "voip_number_action": voip_number_action, + "enable_social": enable_social, + "phone_no_social_presence_action": phone_no_social_presence_action, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncPhone(AsyncAPIResource): + """The ``phone`` resource of the Didit API. + + Methods: ``assess_risk``, ``check_code``, ``send_code``. + """ + + @cached_property + def with_raw_response(self) -> AsyncPhoneWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncPhoneWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncPhoneWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncPhoneWithStreamingResponse(self) + + async def assess_risk( + self, + *, + phone_number: str, + country_code: str | NotGiven = NOT_GIVEN, + signals: RiskSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PhoneRiskResponse: + """Risk-score a phone number. + + Resolve a phone number and return carrier, line-type, porting, disposable-number, cross-session match, and normalized risk-intelligence signals without sending an OTP. The endpoint creates an API session and returns its id as `request_id`. + + **Decision.** The top-level `status` mirrors `phone.status`. It is normally `Approved`; the risk evaluation can produce `In Review` or `Declined`. + + **Compatibility aliases.** Live responses expose the normalized intelligence object as both `phone.phone_intelligence` and `phone.enrichment`. The values are identical. New integrations can read `phone.enrichment`. Both keys are absent when no usable intelligence data was returned. + + **Billing.** Live calls use Phone Risk API pricing. The request is billable once the lookup starts, even when some best-effort fields are unavailable. + + **Sandbox.** Sandbox keys validate the request and return deterministic approved data without sending an OTP, consuming credits, creating a session, or contributing to Networks. + + **Authentication.** Send your application's API key in `x-api-key`. Missing or invalid credentials return `403` - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests per minute per API key across all POST/PATCH/DELETE endpoints. + + **Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price. + + ``POST /v3/phone/risk/`` + + Args: + phone_number: Phone number to score. It is validated and normalized to E.164. + country_code: Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage. + signals: Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks. + vendor_data: Your stable reference for this request. Stored on the API session and echoed in the response. + metadata: Your non-sensitive JSON metadata. Stored on the API session and echoed in the response. + enable_social: When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PhoneRiskResponse", + await self._client.request( + "post", + "/v3/phone/risk/", + cast_to=PhoneRiskResponse, + auth="api_key", + body={ + "phone_number": phone_number, + "country_code": country_code, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + "enable_social": enable_social, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def send_code( + self, + *, + phone_number: str, + options: PhoneSendCodeParamsOptions | NotGiven = NOT_GIVEN, + signals: PhoneSendCodeParamsSignals | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PhoneVerificationSendResponse: + """Send Phone Code. + + Send a one-time passcode (OTP) to a phone number over WhatsApp, SMS, or another supported channel, then verify it with [`POST /v3/phone/check/`](/standalone-apis/phone-check). + + **How send and check pair up.** Verification state is keyed by your application plus the E.164 `phone_number` - the check call does not take `request_id`. Each new send creates a pending verification that lives for **5 minutes**: call the check with the same number and the code the user received before the window closes. Calling send again for the same number while a verification is pending re-sends a code for that same verification (`status: "Retry"`, same `request_id`). At most one retry is attached this way (two sends total); a further send starts a fresh verification with a new `request_id`. The 5-minute window is measured from the first send and is not extended by retries. + + **Delivery channels.** `options.preferred_channel` selects the channel (`whatsapp` by default; `sms`, `telegram`, `voice`, `rcs`, `viber`, and `zalo` are also supported). When the preferred channel is unavailable for the destination country or number, delivery automatically falls back to SMS - the send still reports `Success`, and the channel actually used is reported by the check response (`phone.verification_method` and the `lifecycle` events). + + **Branding.** SMS codes carry your application's SMS sender name, set in the Business Console, at no extra cost. Pass `options.sms_sender_name` to sign one code with another name, or `options.use_white_label_customization: true` to fall back to your white-label public name when no sender name is set. WhatsApp codes use a fixed message and are not branded. + + **Send statuses.** `Success` - OTP sent for a new verification; `Retry` - OTP re-sent for the pending verification; `Blocked` - the anti-fraud layer refused to send (see `reason`, e.g. `repeated_attempts`, `suspicious`, `spam`). A `Blocked` send immediately finalizes the verification as `Declined` with a high-risk warning, so a follow-up check returns `Expired or Not Found`. + + **Billing.** The price depends on the destination country and the channel that actually delivers the message - see [Phone Verification Pricing](/getting-started/phone-verification-pricing). Only `Blocked` sends are guaranteed free: any other unbilled send attempt is charged at verification finalization or expiration, even if delivery was never confirmed or the delivery provider reported the message undeliverable. Checks are always free. As an anti-abuse measure, phone verification is disabled until your organization's first top-up (`403`). + + **Session persistence.** Every new verification is persisted as an API-type session: `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/`, the verification appears in the Business Console, and `status.updated` webhooks fire as it progresses. + + **Sandbox.** Sandbox API keys skip delivery and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Success` payload with a random `request_id`; no message is sent and nothing is persisted. Use code `123456` on the sandbox check. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limits.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints, plus an anti-abuse cap of 4 send attempts per phone number per hour; exceeding either returns `429`. + + ``POST /v3/phone/send/`` + + Args: + phone_number: Recipient phone number in E.164 format - leading `+` and country code (e.g., `+14155552671`). Numbers are validated and normalized to E.164; unparseable or invalid numbers return `400` with a `phone_number` field error. + options: OTP delivery options. All fields are optional. + signals: Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional. + vendor_data: Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record. + metadata: Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PhoneVerificationSendResponse", + await self._client.request( + "post", + "/v3/phone/send/", + cast_to=PhoneVerificationSendResponse, + auth="api_key", + body={ + "phone_number": phone_number, + "options": options, + "signals": signals, + "vendor_data": vendor_data, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def check_code( + self, + *, + phone_number: str, + code: str, + duplicated_phone_number_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + disposable_number_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + voip_number_action: Literal["NO_ACTION", "DECLINE"] | str | NotGiven = NOT_GIVEN, + enable_social: bool | NotGiven = NOT_GIVEN, + phone_no_social_presence_action: Literal["NO_ACTION", "DECLINE"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> PhoneVerificationCheckResponse: + """Check Phone Code. + + Verify the OTP delivered by [`POST /v3/phone/send/`](/standalone-apis/phone-send) and get the final verification result plus phone intelligence: carrier name and line type, virtual (VoIP) and disposable flags, and duplicate usage of the number across your sessions. + + **How the check finds the verification.** Matching is by your application plus the E.164 `phone_number` - `request_id` is not an input. The most recent pending verification created within the last **5 minutes** is checked. If there is none (never sent, already finalized, or older than 5 minutes) the endpoint returns `200` with `status: "Expired or Not Found"`. + + **Attempt budget.** Each verification allows **3 code attempts**. The first two wrong codes return `status: "Failed"` with the attempts remaining and `phone: null`; the third wrong code finalizes the verification as `Declined` with a `VERIFICATION_CODE_ATTEMPTS_EXCEEDED` warning and returns the full phone report. + + **Outcomes.** `Approved` - correct code and no declining risk. `Declined` - terminal: the code was correct but a declining risk matched (a `DECLINE` action below, a blocklisted or high-risk number), or the attempt budget was exhausted. `Failed` - wrong code, attempts remaining. `Expired or Not Found` - nothing to check. On `Approved`/`Declined` the `request_id` equals the send's `request_id` (the session id) and `phone` carries the full report (`carrier`, `is_virtual`, `is_disposable`, `warnings`, `lifecycle`, `matches`); on `Failed` and `Expired or Not Found` the `request_id` is a one-off random UUID. + + **Risk actions.** `duplicated_phone_number_action`, `disposable_number_action`, and `voip_number_action` decide what happens when the corresponding risk is detected on a correct code: `DECLINE` flips the final status to `Declined`; `NO_ACTION` (default) records the risk in `phone.warnings` without affecting the status. + + **Billing.** Checks are free - delivery is billed on the send side (see [Phone Verification Pricing](/getting-started/phone-verification-pricing)). + + **Session persistence.** A finalized check updates the session created by the send (visible in the Business Console, queryable via `GET /v3/session/{sessionId}/decision/`) and fires a `status.updated` webhook. + + **Sandbox.** Sandbox API keys skip all processing: any well-formed code (4–8 digits, `123456` included) returns a static `Approved` payload with a simplified flat `phone` object (`status`, `phone_number`, `country_code`, `carrier_name`, `line_type`, `flags`); malformed input still returns `400`. Nothing is persisted. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limits.** Shared write budget of 300 requests/min per API key, plus the upstream anti-abuse cap of 4 attempts per number per hour; exceeding either returns `429`. + + **Social footprint.** Set `enable_social` to `true` to add a `phone_social` block: the online platforms this number is registered on, with per-category counts and any profile details available. `profiles_registered: 0` means the platforms were checked and the number was found on none of them - a valid, billable answer. The block is absent when the add-on was not requested or the check could not be completed, and it is not billed in that case. Billing adds one `phone_social` unit per request on top of this endpoint's own price. + + ``POST /v3/phone/check/`` + + Args: + phone_number: The same phone number used in the matching [`POST /v3/phone/send/`](/standalone-apis/phone-send) call, in E.164 format. This is what links the check to the send. + code: The OTP the end user received. Must be 4–8 numeric digits - anything else returns `400` without consuming an attempt. A well-formed but wrong code returns `200` with `status: "Failed"` and consumes one of the 3 attempts. + duplicated_phone_number_action: What to do when the same number was already used by a different user (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings` and fills `phone.matches`. + disposable_number_action: What to do when the carrier lookup flags the number as a temporary/burner line (`phone.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`. + voip_number_action: What to do when the carrier lookup reports a virtual line type - `voip`, `isp`, or `vpn` (`phone.is_virtual`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`. + enable_social: When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price. + phone_no_social_presence_action: Action when the social footprint check finds the number registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "PhoneVerificationCheckResponse", + await self._client.request( + "post", + "/v3/phone/check/", + cast_to=PhoneVerificationCheckResponse, + auth="api_key", + body={ + "phone_number": phone_number, + "code": code, + "duplicated_phone_number_action": duplicated_phone_number_action, + "disposable_number_action": disposable_number_action, + "voip_number_action": voip_number_action, + "enable_social": enable_social, + "phone_no_social_presence_action": phone_no_social_presence_action, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class PhoneWithRawResponse: + def __init__(self, resource: Phone) -> None: + self._resource = resource + self.assess_risk = to_raw_response_wrapper(resource.assess_risk) + self.send_code = to_raw_response_wrapper(resource.send_code) + self.check_code = to_raw_response_wrapper(resource.check_code) + + +class AsyncPhoneWithRawResponse: + def __init__(self, resource: AsyncPhone) -> None: + self._resource = resource + self.assess_risk = async_to_raw_response_wrapper(resource.assess_risk) + self.send_code = async_to_raw_response_wrapper(resource.send_code) + self.check_code = async_to_raw_response_wrapper(resource.check_code) + + +class PhoneWithStreamingResponse: + def __init__(self, resource: Phone) -> None: + self._resource = resource + self.assess_risk = to_streamed_response_wrapper(resource.assess_risk) + self.send_code = to_streamed_response_wrapper(resource.send_code) + self.check_code = to_streamed_response_wrapper(resource.check_code) + + +class AsyncPhoneWithStreamingResponse: + def __init__(self, resource: AsyncPhone) -> None: + self._resource = resource + self.assess_risk = async_to_streamed_response_wrapper(resource.assess_risk) + self.send_code = async_to_streamed_response_wrapper(resource.send_code) + self.check_code = async_to_streamed_response_wrapper(resource.check_code) diff --git a/src/didit/resources/proof_of_address.py b/src/didit/resources/proof_of_address.py new file mode 100644 index 0000000..d8f0ead --- /dev/null +++ b/src/didit/resources/proof_of_address.py @@ -0,0 +1,312 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``proof_of_address`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.proof_of_address.verify_response import ProofOfAddressVerifyResponse + +__all__ = [ + "ProofOfAddress", + "AsyncProofOfAddress", + "ProofOfAddressWithRawResponse", + "AsyncProofOfAddressWithRawResponse", + "ProofOfAddressWithStreamingResponse", + "AsyncProofOfAddressWithStreamingResponse", +] + + +class ProofOfAddress(SyncAPIResource): + """The ``proof_of_address`` resource of the Didit API. + + Methods: ``verify``. + """ + + @cached_property + def with_raw_response(self) -> ProofOfAddressWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return ProofOfAddressWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ProofOfAddressWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return ProofOfAddressWithStreamingResponse(self) + + def verify( + self, + *, + document: FileTypes, + document_password: str | NotGiven = NOT_GIVEN, + expected_address: str | NotGiven = NOT_GIVEN, + expected_country: str | NotGiven = NOT_GIVEN, + expected_first_name: str | NotGiven = NOT_GIVEN, + expected_last_name: str | NotGiven = NOT_GIVEN, + poa_languages_allowed: str | NotGiven = NOT_GIVEN, + poa_document_age_months: str | NotGiven = NOT_GIVEN, + poa_name_mismatch_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + poa_document_issues_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + poa_document_authenticity_action: Literal["DECLINE", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + poa_unsupported_language_action: Literal["DECLINE", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + poa_address_mismatch_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + poa_issuer_not_identified_action: Literal["DECLINE", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ProofOfAddressVerifyResponse: + """Proof of Address (document OCR + address checks). + + Extract and validate a Proof of Address document - utility bills, bank statements, and government-issued documents - in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. + + **Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes. + + **How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering - modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports - surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged. + + **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `poa.status` and `poa.warnings`, not just the HTTP code. + + **Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional "Sandbox Power Co." electricity bill), no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + Note: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint - a future-dated document is not auto-declined here. + + ``POST /v3/poa/`` + + Args: + document: The Proof of Address document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`. Maximum upload size: **15 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs require `document_password`. Images are automatically compressed before processing. + document_password: Password to decrypt `document` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`. + expected_address: Address to verify against the document. It is geocoded and compared with the extracted address; a mismatch (or an expected address that cannot be geocoded/verified) adds `ADDRESS_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. The geocoded form is echoed back in `expected_details_formatted_address` / `expected_details_parsed_address`. + expected_country: Country to verify against the document (ISO 3166-1 code, e.g. `ARG` or `AR`). A mismatch with the document's country adds `POA_COUNTRY_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. + expected_first_name: First name to verify against `name_on_document` (fuzzy match, transliteration-aware). A score below the match threshold adds `NAME_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. + expected_last_name: Last name to verify against `name_on_document`, combined with `expected_first_name` for the match score. + poa_languages_allowed: Comma-separated list of accepted document languages as ISO 639-1 codes (e.g. `en,es,fr`). When omitted or blank, **all 51 supported languages** are accepted. A document in a language outside the list adds `UNSUPPORTED_DOCUMENT_LANGUAGE` (governed by `poa_unsupported_language_action`); an unknown code in the list returns `400` with the full set of supported codes. + poa_document_age_months: Comma-separated `key:value` pairs setting the maximum document age in months per document type - keys: `utility_bill`, `bank_statement`, `government_issued_document`, `other_poa_document`; values: `1`–`120`, or `-1` for unlimited (no age check). Defaults when omitted: `utility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12`. A document older than its limit declines with `POA_DOCUMENT_EXPIRED`. **Caution:** when you provide this field, list *every* type you accept - omitted types are treated as not accepted (informational `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`) and their age check is skipped. + poa_name_mismatch_action: Accepted but **currently not applied** - name-mismatch warnings (`NAME_MISMATCH_WITH_PROVIDED`) follow `poa_address_mismatch_action` instead. Set `poa_address_mismatch_action` to control both name and address mismatch behavior. + poa_document_issues_action: What to do on file-integrity problems - in practice only `DOCUMENT_METADATA_MISMATCH` is produced by this endpoint. + poa_document_authenticity_action: What to do when document manipulation is suspected (`SUSPECTED_DOCUMENT_MANIPULATION`) - e.g. the PDF was modified after digital signing, processed by a known PDF editor, shows overlay-text manipulation, or has inconsistent EXIF dates. + poa_unsupported_language_action: What to do when the document language is outside `poa_languages_allowed` (`UNSUPPORTED_DOCUMENT_LANGUAGE`). + poa_address_mismatch_action: What to do when the extracted details disagree with the `expected_*` fields: `ADDRESS_MISMATCH_WITH_PROVIDED`, `NAME_MISMATCH_WITH_PROVIDED`, and `POA_COUNTRY_MISMATCH_WITH_PROVIDED` all follow this action. + poa_issuer_not_identified_action: What to do when no issuing company/organization can be identified on the document (`ISSUER_NOT_IDENTIFIED`). + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the uploaded document stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ProofOfAddressVerifyResponse", + self._client.request( + "post", + "/v3/poa/", + cast_to=ProofOfAddressVerifyResponse, + auth="api_key", + body={ + "document": document, + "document_password": document_password, + "expected_address": expected_address, + "expected_country": expected_country, + "expected_first_name": expected_first_name, + "expected_last_name": expected_last_name, + "poa_languages_allowed": poa_languages_allowed, + "poa_document_age_months": poa_document_age_months, + "poa_name_mismatch_action": poa_name_mismatch_action, + "poa_document_issues_action": poa_document_issues_action, + "poa_document_authenticity_action": poa_document_authenticity_action, + "poa_unsupported_language_action": poa_unsupported_language_action, + "poa_address_mismatch_action": poa_address_mismatch_action, + "poa_issuer_not_identified_action": poa_issuer_not_identified_action, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("document",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncProofOfAddress(AsyncAPIResource): + """The ``proof_of_address`` resource of the Didit API. + + Methods: ``verify``. + """ + + @cached_property + def with_raw_response(self) -> AsyncProofOfAddressWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncProofOfAddressWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncProofOfAddressWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncProofOfAddressWithStreamingResponse(self) + + async def verify( + self, + *, + document: FileTypes, + document_password: str | NotGiven = NOT_GIVEN, + expected_address: str | NotGiven = NOT_GIVEN, + expected_country: str | NotGiven = NOT_GIVEN, + expected_first_name: str | NotGiven = NOT_GIVEN, + expected_last_name: str | NotGiven = NOT_GIVEN, + poa_languages_allowed: str | NotGiven = NOT_GIVEN, + poa_document_age_months: str | NotGiven = NOT_GIVEN, + poa_name_mismatch_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + poa_document_issues_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + poa_document_authenticity_action: Literal["DECLINE", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + poa_unsupported_language_action: Literal["DECLINE", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + poa_address_mismatch_action: Literal["DECLINE", "NO_ACTION"] | str | NotGiven = NOT_GIVEN, + poa_issuer_not_identified_action: Literal["DECLINE", "NO_ACTION"] + | str + | NotGiven = NOT_GIVEN, + save_api_request: bool | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ProofOfAddressVerifyResponse: + """Proof of Address (document OCR + address checks). + + Extract and validate a Proof of Address document - utility bills, bank statements, and government-issued documents - in one call. Returns the issuer, issue/expiry dates, the holder name, the raw and parsed/geocoded address, an `Approved`/`Declined` `status`, and a `warnings` list explaining every issue found. + + **Latency.** Extraction is LLM-based: typical calls take **5–15 seconds**, multi-page PDFs up to **~30 seconds**. Configure a client timeout of **at least 45 seconds** and do not retry before the call completes. + + **How it works.** The document (image or PDF, up to 15 MB; encrypted PDFs require `document_password`) is classified into a document type and subtype (e.g. `UTILITY_BILL` / `WATER_BILL`), and the issuer, dates, holder name, and address are extracted with LLM-based extraction. The address is parsed and geocoded into `poa_parsed_address`. PDF/EXIF forensics screen for tampering - modification after digital signing, known PDF editors, overlay-text manipulation, suspicious re-exports - surfacing `SUSPECTED_DOCUMENT_MANIPULATION` or `DOCUMENT_METADATA_MISMATCH`. Document age is validated against `poa_document_age_months`, and the language against `poa_languages_allowed`. If you pass `expected_address`, `expected_country`, or `expected_first_name`/`expected_last_name`, the extracted values are cross-checked and mismatches are flagged. + + **Decision logic.** `status` is `Approved` unless at least one warning resolves to a decline. Hard failures always decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (not configurable on this endpoint). Five risk groups are configurable per request via the `poa_*_action` options (`DECLINE` or `NO_ACTION`, all defaulting to `DECLINE`). `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION` and `UNPARSABLE_OR_INVALID_ADDRESS` are informational on this endpoint and never decline. A document that cannot be processed at all returns `400`; a readable but problematic document returns `200` with `status: "Declined"` - always inspect `poa.status` and `poa.warnings`, not just the HTTP code. + + **Billing.** Each `200` response consumes one Proof of Address API credit (standalone APIs have no free tier). When the organization's balance cannot cover the call, the endpoint returns `403` with the not-enough-credits error before any document processing. + + **Session persistence (`save_api_request`, default `true`).** When `true`, the call is persisted as an API-type session: it appears in the Business Console, the uploaded document is stored, the returned `request_id` is a real session id you can pass to `GET /v3/session/{sessionId}/decision/` (the decision additionally exposes `document_file` and the `document_metadata` forensics, which this response omits), and a `status.updated` webhook is emitted. When `false`, nothing is stored and `request_id` is a one-off correlation UUID that cannot be looked up later. + + **Sandbox.** Sandbox API keys skip all processing and billing: after request validation (malformed input still returns `400`), the endpoint returns a static `Approved` mock payload (a fictional "Sandbox Power Co." electricity bill), no session is persisted, and no credits are consumed. + + **Authentication.** Send your application's API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`) - this API never returns `401`. + + **Rate limit.** Shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + Note: `FUTURE_ISSUE_DATE` and `POOR_DOCUMENT_QUALITY` exist in workflow sessions but are never produced by this standalone endpoint - a future-dated document is not auto-declined here. + + ``POST /v3/poa/`` + + Args: + document: The Proof of Address document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`. Maximum upload size: **15 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs require `document_password`. Images are automatically compressed before processing. + document_password: Password to decrypt `document` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`. + expected_address: Address to verify against the document. It is geocoded and compared with the extracted address; a mismatch (or an expected address that cannot be geocoded/verified) adds `ADDRESS_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. The geocoded form is echoed back in `expected_details_formatted_address` / `expected_details_parsed_address`. + expected_country: Country to verify against the document (ISO 3166-1 code, e.g. `ARG` or `AR`). A mismatch with the document's country adds `POA_COUNTRY_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. + expected_first_name: First name to verify against `name_on_document` (fuzzy match, transliteration-aware). A score below the match threshold adds `NAME_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. + expected_last_name: Last name to verify against `name_on_document`, combined with `expected_first_name` for the match score. + poa_languages_allowed: Comma-separated list of accepted document languages as ISO 639-1 codes (e.g. `en,es,fr`). When omitted or blank, **all 51 supported languages** are accepted. A document in a language outside the list adds `UNSUPPORTED_DOCUMENT_LANGUAGE` (governed by `poa_unsupported_language_action`); an unknown code in the list returns `400` with the full set of supported codes. + poa_document_age_months: Comma-separated `key:value` pairs setting the maximum document age in months per document type - keys: `utility_bill`, `bank_statement`, `government_issued_document`, `other_poa_document`; values: `1`–`120`, or `-1` for unlimited (no age check). Defaults when omitted: `utility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12`. A document older than its limit declines with `POA_DOCUMENT_EXPIRED`. **Caution:** when you provide this field, list *every* type you accept - omitted types are treated as not accepted (informational `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`) and their age check is skipped. + poa_name_mismatch_action: Accepted but **currently not applied** - name-mismatch warnings (`NAME_MISMATCH_WITH_PROVIDED`) follow `poa_address_mismatch_action` instead. Set `poa_address_mismatch_action` to control both name and address mismatch behavior. + poa_document_issues_action: What to do on file-integrity problems - in practice only `DOCUMENT_METADATA_MISMATCH` is produced by this endpoint. + poa_document_authenticity_action: What to do when document manipulation is suspected (`SUSPECTED_DOCUMENT_MANIPULATION`) - e.g. the PDF was modified after digital signing, processed by a known PDF editor, shows overlay-text manipulation, or has inconsistent EXIF dates. + poa_unsupported_language_action: What to do when the document language is outside `poa_languages_allowed` (`UNSUPPORTED_DOCUMENT_LANGUAGE`). + poa_address_mismatch_action: What to do when the extracted details disagree with the `expected_*` fields: `ADDRESS_MISMATCH_WITH_PROVIDED`, `NAME_MISMATCH_WITH_PROVIDED`, and `POA_COUNTRY_MISMATCH_WITH_PROVIDED` all follow this action. + poa_issuer_not_identified_action: What to do when no issuing company/organization can be identified on the document (`ISSUER_NOT_IDENTIFIED`). + save_api_request: When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the uploaded document stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only. + vendor_data: Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later. + metadata: Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ProofOfAddressVerifyResponse", + await self._client.request( + "post", + "/v3/poa/", + cast_to=ProofOfAddressVerifyResponse, + auth="api_key", + body={ + "document": document, + "document_password": document_password, + "expected_address": expected_address, + "expected_country": expected_country, + "expected_first_name": expected_first_name, + "expected_last_name": expected_last_name, + "poa_languages_allowed": poa_languages_allowed, + "poa_document_age_months": poa_document_age_months, + "poa_name_mismatch_action": poa_name_mismatch_action, + "poa_document_issues_action": poa_document_issues_action, + "poa_document_authenticity_action": poa_document_authenticity_action, + "poa_unsupported_language_action": poa_unsupported_language_action, + "poa_address_mismatch_action": poa_address_mismatch_action, + "poa_issuer_not_identified_action": poa_issuer_not_identified_action, + "save_api_request": save_api_request, + "vendor_data": vendor_data, + "metadata": metadata, + }, + multipart=True, + file_fields=("document",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class ProofOfAddressWithRawResponse: + def __init__(self, resource: ProofOfAddress) -> None: + self._resource = resource + self.verify = to_raw_response_wrapper(resource.verify) + + +class AsyncProofOfAddressWithRawResponse: + def __init__(self, resource: AsyncProofOfAddress) -> None: + self._resource = resource + self.verify = async_to_raw_response_wrapper(resource.verify) + + +class ProofOfAddressWithStreamingResponse: + def __init__(self, resource: ProofOfAddress) -> None: + self._resource = resource + self.verify = to_streamed_response_wrapper(resource.verify) + + +class AsyncProofOfAddressWithStreamingResponse: + def __init__(self, resource: AsyncProofOfAddress) -> None: + self._resource = resource + self.verify = async_to_streamed_response_wrapper(resource.verify) diff --git a/src/didit/resources/questionnaires.py b/src/didit/resources/questionnaires.py new file mode 100644 index 0000000..fa2f633 --- /dev/null +++ b/src/didit/resources/questionnaires.py @@ -0,0 +1,568 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``questionnaires`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.questionnaire_detail import QuestionnaireDetail +from didit.types.components.questionnaire_list_item import QuestionnaireListItem +from didit.types.components.simple_questionnaire_element import SimpleQuestionnaireElement +from didit.types.questionnaires.list_response import QuestionnairesListResponse + +__all__ = [ + "Questionnaires", + "AsyncQuestionnaires", + "QuestionnairesWithRawResponse", + "AsyncQuestionnairesWithRawResponse", + "QuestionnairesWithStreamingResponse", + "AsyncQuestionnairesWithStreamingResponse", +] + + +class Questionnaires(SyncAPIResource): + """The ``questionnaires`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> QuestionnairesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return QuestionnairesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> QuestionnairesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return QuestionnairesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[QuestionnaireListItem]: + """List questionnaires. + + List your questionnaires, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `questionnaire_group_id` is the stable group ID resolved to the latest `published` version. + + ``GET /v3/questionnaires/`` + + Args: + limit: Page size (number of questionnaires per page). + offset: Number of rows to skip before the first returned questionnaire. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[QuestionnaireListItem]", + self._client.request_page( + "get", + "/v3/questionnaires/", + cast_to=QuestionnairesListResponse, + item_type=QuestionnaireListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + title: str, + languages: Sequence[str], + form_elements: Sequence[SimpleQuestionnaireElement], + description: str | None | NotGiven = NOT_GIVEN, + default_language: str | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> QuestionnaireDetail: + """Create questionnaire. + + Create a simple linear questionnaire. Send `form_elements` in display order; branching and repeatable groups require the Console. Defaults to `published` v1. + + ``POST /v3/questionnaires/`` + + Args: + title: Internal name for the questionnaire. + languages: Languages included in every label. Must include `en`. + form_elements: Questions in the exact order users should see them. Branching and conditional fields are not supported. + description + default_language: Default language. Must be included in `languages`. + is_active + status: Omit this field to create a published questionnaire ready for workflows. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "QuestionnaireDetail", + self._client.request( + "post", + "/v3/questionnaires/", + cast_to=QuestionnaireDetail, + auth="api_key", + body={ + "title": title, + "languages": languages, + "form_elements": form_elements, + "description": description, + "default_language": default_language, + "is_active": is_active, + "status": status, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + questionnaire_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> QuestionnaireDetail: + """Get questionnaire. + + Fetch full questionnaire detail for one version by `questionnaire_uuid`: title, `languages`, canonical `graph`, derived `sections`, and version metadata. + + ``GET /v3/questionnaires/{questionnaire_uuid}/`` + + Args: + questionnaire_uuid: Per-version UUID of the questionnaire (the `uuid` value returned by the list endpoint, also returned as `questionnaire_id` on detail endpoints). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "QuestionnaireDetail", + self._client.request( + "get", + "/v3/questionnaires/{questionnaire_uuid}/", + cast_to=QuestionnaireDetail, + path_params={"questionnaire_uuid": questionnaire_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + questionnaire_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete questionnaire. + + Permanently delete one questionnaire version (no undo). Stored questionnaire responses linked to this version are deleted with it, and sessions referencing it lose the link; other versions in the same group are not deleted. Workflows that referenced it keep running but the questionnaire step can no longer load it. + + ``DELETE /v3/questionnaires/{questionnaire_uuid}/`` + + Args: + questionnaire_uuid: Per-version UUID of the questionnaire to delete. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/questionnaires/{questionnaire_uuid}/", + cast_to=None, + path_params={"questionnaire_uuid": questionnaire_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + questionnaire_uuid: str, + *, + title: str | NotGiven = NOT_GIVEN, + description: str | None | NotGiven = NOT_GIVEN, + languages: Sequence[str] | NotGiven = NOT_GIVEN, + default_language: str | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + form_elements: Sequence[SimpleQuestionnaireElement] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> QuestionnaireDetail: + """Update questionnaire. + + Update a questionnaire. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `questionnaire_id`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: "published"` on a **published** version → a NEW published version is created under the same `questionnaire_group_id` (new `questionnaire_id`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: "draft"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`"Cannot revert a published version to draft."`). + + ``PATCH /v3/questionnaires/{questionnaire_uuid}/`` + + Args: + questionnaire_uuid: Per-version UUID of the questionnaire to update. + title + description + languages + default_language + is_active + status + form_elements: If provided, this replaces the full linear question list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "QuestionnaireDetail", + self._client.request( + "patch", + "/v3/questionnaires/{questionnaire_uuid}/", + cast_to=QuestionnaireDetail, + path_params={"questionnaire_uuid": questionnaire_uuid}, + auth="api_key", + body={ + "title": title, + "description": description, + "languages": languages, + "default_language": default_language, + "is_active": is_active, + "status": status, + "form_elements": form_elements, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncQuestionnaires(AsyncAPIResource): + """The ``questionnaires`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncQuestionnairesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncQuestionnairesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncQuestionnairesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncQuestionnairesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[QuestionnaireListItem]: + """List questionnaires. + + List your questionnaires, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `questionnaire_group_id` is the stable group ID resolved to the latest `published` version. + + ``GET /v3/questionnaires/`` + + Args: + limit: Page size (number of questionnaires per page). + offset: Number of rows to skip before the first returned questionnaire. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[QuestionnaireListItem]", + self._client.request_page( + "get", + "/v3/questionnaires/", + cast_to=QuestionnairesListResponse, + item_type=QuestionnaireListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + title: str, + languages: Sequence[str], + form_elements: Sequence[SimpleQuestionnaireElement], + description: str | None | NotGiven = NOT_GIVEN, + default_language: str | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> QuestionnaireDetail: + """Create questionnaire. + + Create a simple linear questionnaire. Send `form_elements` in display order; branching and repeatable groups require the Console. Defaults to `published` v1. + + ``POST /v3/questionnaires/`` + + Args: + title: Internal name for the questionnaire. + languages: Languages included in every label. Must include `en`. + form_elements: Questions in the exact order users should see them. Branching and conditional fields are not supported. + description + default_language: Default language. Must be included in `languages`. + is_active + status: Omit this field to create a published questionnaire ready for workflows. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "QuestionnaireDetail", + await self._client.request( + "post", + "/v3/questionnaires/", + cast_to=QuestionnaireDetail, + auth="api_key", + body={ + "title": title, + "languages": languages, + "form_elements": form_elements, + "description": description, + "default_language": default_language, + "is_active": is_active, + "status": status, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + questionnaire_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> QuestionnaireDetail: + """Get questionnaire. + + Fetch full questionnaire detail for one version by `questionnaire_uuid`: title, `languages`, canonical `graph`, derived `sections`, and version metadata. + + ``GET /v3/questionnaires/{questionnaire_uuid}/`` + + Args: + questionnaire_uuid: Per-version UUID of the questionnaire (the `uuid` value returned by the list endpoint, also returned as `questionnaire_id` on detail endpoints). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "QuestionnaireDetail", + await self._client.request( + "get", + "/v3/questionnaires/{questionnaire_uuid}/", + cast_to=QuestionnaireDetail, + path_params={"questionnaire_uuid": questionnaire_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + questionnaire_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete questionnaire. + + Permanently delete one questionnaire version (no undo). Stored questionnaire responses linked to this version are deleted with it, and sessions referencing it lose the link; other versions in the same group are not deleted. Workflows that referenced it keep running but the questionnaire step can no longer load it. + + ``DELETE /v3/questionnaires/{questionnaire_uuid}/`` + + Args: + questionnaire_uuid: Per-version UUID of the questionnaire to delete. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/questionnaires/{questionnaire_uuid}/", + cast_to=None, + path_params={"questionnaire_uuid": questionnaire_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + questionnaire_uuid: str, + *, + title: str | NotGiven = NOT_GIVEN, + description: str | None | NotGiven = NOT_GIVEN, + languages: Sequence[str] | NotGiven = NOT_GIVEN, + default_language: str | NotGiven = NOT_GIVEN, + is_active: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + form_elements: Sequence[SimpleQuestionnaireElement] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> QuestionnaireDetail: + """Update questionnaire. + + Update a questionnaire. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `questionnaire_id`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: "published"` on a **published** version → a NEW published version is created under the same `questionnaire_group_id` (new `questionnaire_id`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: "draft"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`"Cannot revert a published version to draft."`). + + ``PATCH /v3/questionnaires/{questionnaire_uuid}/`` + + Args: + questionnaire_uuid: Per-version UUID of the questionnaire to update. + title + description + languages + default_language + is_active + status + form_elements: If provided, this replaces the full linear question list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "QuestionnaireDetail", + await self._client.request( + "patch", + "/v3/questionnaires/{questionnaire_uuid}/", + cast_to=QuestionnaireDetail, + path_params={"questionnaire_uuid": questionnaire_uuid}, + auth="api_key", + body={ + "title": title, + "description": description, + "languages": languages, + "default_language": default_language, + "is_active": is_active, + "status": status, + "form_elements": form_elements, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class QuestionnairesWithRawResponse: + def __init__(self, resource: Questionnaires) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncQuestionnairesWithRawResponse: + def __init__(self, resource: AsyncQuestionnaires) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + + +class QuestionnairesWithStreamingResponse: + def __init__(self, resource: Questionnaires) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncQuestionnairesWithStreamingResponse: + def __init__(self, resource: AsyncQuestionnaires) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/regulatory_reports.py b/src/didit/resources/regulatory_reports.py new file mode 100644 index 0000000..15b0062 --- /dev/null +++ b/src/didit/resources/regulatory_reports.py @@ -0,0 +1,995 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``regulatory_reports`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.regulatory_report_detail import RegulatoryReportDetail +from didit.types.regulatory_reports.create_params import RegulatoryReportsCreateParamsNarrative +from didit.types.regulatory_reports.list_response import RegulatoryReportsListResponse +from didit.types.regulatory_reports.retrieve_download_url_response import ( + RegulatoryReportsRetrieveDownloadUrlResponse, +) +from didit.types.regulatory_reports.update_params import RegulatoryReportsUpdateParamsNarrative +from didit.types.regulatory_reports.validate_response import RegulatoryReportsValidateResponse + +__all__ = [ + "RegulatoryReports", + "AsyncRegulatoryReports", + "RegulatoryReportsWithRawResponse", + "AsyncRegulatoryReportsWithRawResponse", + "RegulatoryReportsWithStreamingResponse", + "AsyncRegulatoryReportsWithStreamingResponse", +] + + +class RegulatoryReports(SyncAPIResource): + """The ``regulatory_reports`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``finalize``, ``list``, ``retrieve``, ``retrieve_download_url``, ``update``, ``validate``. + """ + + @cached_property + def with_raw_response(self) -> RegulatoryReportsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return RegulatoryReportsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> RegulatoryReportsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return RegulatoryReportsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: Literal["DRAFT", "VALIDATED", "FINALIZED"] | str | NotGiven = NOT_GIVEN, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + case: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[RegulatoryReportDetail]: + """List reports. + + Application-wide FIU report list, optionally filtered by status, report_type, or case. + + ``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/`` + + Args: + limit: Page size. + offset: Zero-based offset. + status: Filter by status. + report_type: Filter by report type. + case: Filter to reports on this case. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[RegulatoryReportDetail]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/", + cast_to=RegulatoryReportsListResponse, + item_type=RegulatoryReportDetail, + scope="application", + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "status": status, + "report_type": report_type, + "case": case, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + case: str, + template: str, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + transactions: Sequence[str] | None | NotGiven = NOT_GIVEN, + rentity_id: str | None | NotGiven = NOT_GIVEN, + rentity_branch: str | None | NotGiven = NOT_GIVEN, + submission_code: Literal["E", "M"] | str | NotGiven = NOT_GIVEN, + entity_reference: str | None | NotGiven = NOT_GIVEN, + fiu_ref_number: str | None | NotGiven = NOT_GIVEN, + currency_code_local: str | None | NotGiven = NOT_GIVEN, + submission_date: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + reporting_person: Mapping[str, object] | NotGiven = NOT_GIVEN, + location: Mapping[str, object] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, + action: str | None | NotGiven = NOT_GIVEN, + activity: str | None | NotGiven = NOT_GIVEN, + narrative: RegulatoryReportsCreateParamsNarrative | NotGiven = NOT_GIVEN, + report_indicators: Sequence[str] | NotGiven = NOT_GIVEN, + included_fields: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Create standalone report. + + Create a draft FIU report from the standalone Reports page: both case and template are required (unlike the case-scoped create, which can omit template). + + ``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/`` + + Args: + case + template + report_type: Required unless a template is given; the template's report_type is used otherwise. + jurisdiction: Required unless a template is given. + transactions: Transactions to attach; defaults to the case's linked transactions. + rentity_id + rentity_branch + submission_code + entity_reference + fiu_ref_number + currency_code_local + submission_date + reporting_person + location + reason + action + activity + narrative + report_indicators + included_fields + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/", + cast_to=RegulatoryReportDetail, + scope="application", + auth="api_key", + body={ + "case": case, + "template": template, + "report_type": report_type, + "jurisdiction": jurisdiction, + "transactions": transactions, + "rentity_id": rentity_id, + "rentity_branch": rentity_branch, + "submission_code": submission_code, + "entity_reference": entity_reference, + "fiu_ref_number": fiu_ref_number, + "currency_code_local": currency_code_local, + "submission_date": submission_date, + "reporting_person": reporting_person, + "location": location, + "reason": reason, + "action": action, + "activity": activity, + "narrative": narrative, + "report_indicators": report_indicators, + "included_fields": included_fields, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Get report. + + Retrieve a regulatory report. + + ``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", + cast_to=RegulatoryReportDetail, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete report. + + Delete a draft or validated report. Finalized reports cannot be deleted (409). + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", + cast_to=None, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + report_uuid: str, + *, + jurisdiction: str | NotGiven = NOT_GIVEN, + rentity_id: str | None | NotGiven = NOT_GIVEN, + rentity_branch: str | None | NotGiven = NOT_GIVEN, + submission_code: Literal["E", "M"] | str | NotGiven = NOT_GIVEN, + entity_reference: str | None | NotGiven = NOT_GIVEN, + fiu_ref_number: str | None | NotGiven = NOT_GIVEN, + currency_code_local: str | None | NotGiven = NOT_GIVEN, + submission_date: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + reporting_person: Mapping[str, object] | NotGiven = NOT_GIVEN, + location: Mapping[str, object] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, + action: str | None | NotGiven = NOT_GIVEN, + activity: str | None | NotGiven = NOT_GIVEN, + narrative: RegulatoryReportsUpdateParamsNarrative | NotGiven = NOT_GIVEN, + report_indicators: Sequence[str] | NotGiven = NOT_GIVEN, + included_fields: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Update report. + + Edit a draft or validated report's fields. Finalized reports cannot be edited (409). + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/`` + + Args: + report_uuid: Report UUID. + jurisdiction + rentity_id + rentity_branch + submission_code + entity_reference + fiu_ref_number + currency_code_local + submission_date + reporting_person + location + reason + action + activity + narrative + report_indicators + included_fields + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", + cast_to=RegulatoryReportDetail, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + body={ + "jurisdiction": jurisdiction, + "rentity_id": rentity_id, + "rentity_branch": rentity_branch, + "submission_code": submission_code, + "entity_reference": entity_reference, + "fiu_ref_number": fiu_ref_number, + "currency_code_local": currency_code_local, + "submission_date": submission_date, + "reporting_person": reporting_person, + "location": location, + "reason": reason, + "action": action, + "activity": activity, + "narrative": narrative, + "report_indicators": report_indicators, + "included_fields": included_fields, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def validate( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportsValidateResponse: + """Validate report. + + Check the report against the obligatory goAML header fields (reporting entity ID, submission code, local currency, action taken, submission date), plus at least one attached transaction and a 2-letter jurisdiction, without generating any files. Also stores the result on the report: validation_errors and validated_at are updated, and a non-finalized report's status moves to VALIDATED (no errors) or back to DRAFT (errors). + + ``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportsValidateResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/", + cast_to=RegulatoryReportsValidateResponse, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def finalize( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Finalize report. + + Re-validate, then render the goAML XML and PDF and upload both to S3. On SAR/STR/TFR reports, this also moves the case's transaction alerts from Pending SAR to SAR filed. + + ``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/", + cast_to=RegulatoryReportDetail, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_download_url( + self, + report_uuid: str, + *, + file: Literal["pdf", "xml"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportsRetrieveDownloadUrlResponse: + """Get report download URL. + + Mint a presigned URL for a finalized report's goAML XML or PDF. + + ``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/`` + + Args: + report_uuid: Report UUID. + file: Which file to download. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportsRetrieveDownloadUrlResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/", + cast_to=RegulatoryReportsRetrieveDownloadUrlResponse, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + query={"file": file}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncRegulatoryReports(AsyncAPIResource): + """The ``regulatory_reports`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``finalize``, ``list``, ``retrieve``, ``retrieve_download_url``, ``update``, ``validate``. + """ + + @cached_property + def with_raw_response(self) -> AsyncRegulatoryReportsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncRegulatoryReportsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncRegulatoryReportsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncRegulatoryReportsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: Literal["DRAFT", "VALIDATED", "FINALIZED"] | str | NotGiven = NOT_GIVEN, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + case: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[RegulatoryReportDetail]: + """List reports. + + Application-wide FIU report list, optionally filtered by status, report_type, or case. + + ``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/`` + + Args: + limit: Page size. + offset: Zero-based offset. + status: Filter by status. + report_type: Filter by report type. + case: Filter to reports on this case. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[RegulatoryReportDetail]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/", + cast_to=RegulatoryReportsListResponse, + item_type=RegulatoryReportDetail, + scope="application", + auth="api_key", + query={ + "limit": limit, + "offset": offset, + "status": status, + "report_type": report_type, + "case": case, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + case: str, + template: str, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + transactions: Sequence[str] | None | NotGiven = NOT_GIVEN, + rentity_id: str | None | NotGiven = NOT_GIVEN, + rentity_branch: str | None | NotGiven = NOT_GIVEN, + submission_code: Literal["E", "M"] | str | NotGiven = NOT_GIVEN, + entity_reference: str | None | NotGiven = NOT_GIVEN, + fiu_ref_number: str | None | NotGiven = NOT_GIVEN, + currency_code_local: str | None | NotGiven = NOT_GIVEN, + submission_date: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + reporting_person: Mapping[str, object] | NotGiven = NOT_GIVEN, + location: Mapping[str, object] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, + action: str | None | NotGiven = NOT_GIVEN, + activity: str | None | NotGiven = NOT_GIVEN, + narrative: RegulatoryReportsCreateParamsNarrative | NotGiven = NOT_GIVEN, + report_indicators: Sequence[str] | NotGiven = NOT_GIVEN, + included_fields: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Create standalone report. + + Create a draft FIU report from the standalone Reports page: both case and template are required (unlike the case-scoped create, which can omit template). + + ``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/`` + + Args: + case + template + report_type: Required unless a template is given; the template's report_type is used otherwise. + jurisdiction: Required unless a template is given. + transactions: Transactions to attach; defaults to the case's linked transactions. + rentity_id + rentity_branch + submission_code + entity_reference + fiu_ref_number + currency_code_local + submission_date + reporting_person + location + reason + action + activity + narrative + report_indicators + included_fields + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/", + cast_to=RegulatoryReportDetail, + scope="application", + auth="api_key", + body={ + "case": case, + "template": template, + "report_type": report_type, + "jurisdiction": jurisdiction, + "transactions": transactions, + "rentity_id": rentity_id, + "rentity_branch": rentity_branch, + "submission_code": submission_code, + "entity_reference": entity_reference, + "fiu_ref_number": fiu_ref_number, + "currency_code_local": currency_code_local, + "submission_date": submission_date, + "reporting_person": reporting_person, + "location": location, + "reason": reason, + "action": action, + "activity": activity, + "narrative": narrative, + "report_indicators": report_indicators, + "included_fields": included_fields, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Get report. + + Retrieve a regulatory report. + + ``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", + cast_to=RegulatoryReportDetail, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete report. + + Delete a draft or validated report. Finalized reports cannot be deleted (409). + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", + cast_to=None, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + report_uuid: str, + *, + jurisdiction: str | NotGiven = NOT_GIVEN, + rentity_id: str | None | NotGiven = NOT_GIVEN, + rentity_branch: str | None | NotGiven = NOT_GIVEN, + submission_code: Literal["E", "M"] | str | NotGiven = NOT_GIVEN, + entity_reference: str | None | NotGiven = NOT_GIVEN, + fiu_ref_number: str | None | NotGiven = NOT_GIVEN, + currency_code_local: str | None | NotGiven = NOT_GIVEN, + submission_date: str | _dt.datetime | None | NotGiven = NOT_GIVEN, + reporting_person: Mapping[str, object] | NotGiven = NOT_GIVEN, + location: Mapping[str, object] | NotGiven = NOT_GIVEN, + reason: str | None | NotGiven = NOT_GIVEN, + action: str | None | NotGiven = NOT_GIVEN, + activity: str | None | NotGiven = NOT_GIVEN, + narrative: RegulatoryReportsUpdateParamsNarrative | NotGiven = NOT_GIVEN, + report_indicators: Sequence[str] | NotGiven = NOT_GIVEN, + included_fields: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Update report. + + Edit a draft or validated report's fields. Finalized reports cannot be edited (409). + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/`` + + Args: + report_uuid: Report UUID. + jurisdiction + rentity_id + rentity_branch + submission_code + entity_reference + fiu_ref_number + currency_code_local + submission_date + reporting_person + location + reason + action + activity + narrative + report_indicators + included_fields + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/", + cast_to=RegulatoryReportDetail, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + body={ + "jurisdiction": jurisdiction, + "rentity_id": rentity_id, + "rentity_branch": rentity_branch, + "submission_code": submission_code, + "entity_reference": entity_reference, + "fiu_ref_number": fiu_ref_number, + "currency_code_local": currency_code_local, + "submission_date": submission_date, + "reporting_person": reporting_person, + "location": location, + "reason": reason, + "action": action, + "activity": activity, + "narrative": narrative, + "report_indicators": report_indicators, + "included_fields": included_fields, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def validate( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportsValidateResponse: + """Validate report. + + Check the report against the obligatory goAML header fields (reporting entity ID, submission code, local currency, action taken, submission date), plus at least one attached transaction and a 2-letter jurisdiction, without generating any files. Also stores the result on the report: validation_errors and validated_at are updated, and a non-finalized report's status moves to VALIDATED (no errors) or back to DRAFT (errors). + + ``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportsValidateResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/", + cast_to=RegulatoryReportsValidateResponse, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def finalize( + self, + report_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportDetail: + """Finalize report. + + Re-validate, then render the goAML XML and PDF and upload both to S3. On SAR/STR/TFR reports, this also moves the case's transaction alerts from Pending SAR to SAR filed. + + ``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/`` + + Args: + report_uuid: Report UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/", + cast_to=RegulatoryReportDetail, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_download_url( + self, + report_uuid: str, + *, + file: Literal["pdf", "xml"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> RegulatoryReportsRetrieveDownloadUrlResponse: + """Get report download URL. + + Mint a presigned URL for a finalized report's goAML XML or PDF. + + ``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/`` + + Args: + report_uuid: Report UUID. + file: Which file to download. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "RegulatoryReportsRetrieveDownloadUrlResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/", + cast_to=RegulatoryReportsRetrieveDownloadUrlResponse, + path_params={"report_uuid": report_uuid}, + scope="application", + auth="api_key", + query={"file": file}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class RegulatoryReportsWithRawResponse: + def __init__(self, resource: RegulatoryReports) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + self.validate = to_raw_response_wrapper(resource.validate) + self.finalize = to_raw_response_wrapper(resource.finalize) + self.retrieve_download_url = to_raw_response_wrapper(resource.retrieve_download_url) + + +class AsyncRegulatoryReportsWithRawResponse: + def __init__(self, resource: AsyncRegulatoryReports) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + self.validate = async_to_raw_response_wrapper(resource.validate) + self.finalize = async_to_raw_response_wrapper(resource.finalize) + self.retrieve_download_url = async_to_raw_response_wrapper(resource.retrieve_download_url) + + +class RegulatoryReportsWithStreamingResponse: + def __init__(self, resource: RegulatoryReports) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + self.validate = to_streamed_response_wrapper(resource.validate) + self.finalize = to_streamed_response_wrapper(resource.finalize) + self.retrieve_download_url = to_streamed_response_wrapper(resource.retrieve_download_url) + + +class AsyncRegulatoryReportsWithStreamingResponse: + def __init__(self, resource: AsyncRegulatoryReports) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) + self.validate = async_to_streamed_response_wrapper(resource.validate) + self.finalize = async_to_streamed_response_wrapper(resource.finalize) + self.retrieve_download_url = async_to_streamed_response_wrapper( + resource.retrieve_download_url + ) diff --git a/src/didit/resources/report_templates.py b/src/didit/resources/report_templates.py new file mode 100644 index 0000000..2f82b37 --- /dev/null +++ b/src/didit/resources/report_templates.py @@ -0,0 +1,544 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``report_templates`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.report_template_detail import ReportTemplateDetail +from didit.types.report_templates.list_response import ReportTemplatesListResponse + +__all__ = [ + "ReportTemplates", + "AsyncReportTemplates", + "ReportTemplatesWithRawResponse", + "AsyncReportTemplatesWithRawResponse", + "ReportTemplatesWithStreamingResponse", + "AsyncReportTemplatesWithStreamingResponse", +] + + +class ReportTemplates(SyncAPIResource): + """The ``report_templates`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> ReportTemplatesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return ReportTemplatesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> ReportTemplatesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return ReportTemplatesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[ReportTemplateDetail]: + """List report templates. + + List FIU report templates for the application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/report-templates/`` + + Args: + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[ReportTemplateDetail]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/", + cast_to=ReportTemplatesListResponse, + item_type=ReportTemplateDetail, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + name: str, + jurisdiction: str, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str, + field_config: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReportTemplateDetail: + """Create report template. + + Create a reusable per-jurisdiction, per-report-type goAML header template. Rejected with 400 if any of the template-level obligatory fields (rentity_id, submission_code, currency_code_local) is missing from field_config; an invalid template cannot be saved or used to create a report. Per-filing header fields (action, submission_date, transactions) are enforced at report validate/finalize time instead. + + ``POST /v3/organization/{organization_id}/application/{application_id}/report-templates/`` + + Args: + name + jurisdiction: ISO 3166-1 alpha-2 country code, upper-cased on save. + report_type + field_config: Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ReportTemplateDetail", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/", + cast_to=ReportTemplateDetail, + scope="application", + auth="api_key", + body={ + "name": name, + "jurisdiction": jurisdiction, + "report_type": report_type, + "field_config": field_config, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReportTemplateDetail: + """Get report template. + + Retrieve a report template. + + ``GET /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/`` + + Args: + template_uuid: Report template UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ReportTemplateDetail", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/", + cast_to=ReportTemplateDetail, + path_params={"template_uuid": template_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete report template. + + Delete a report template. Reports already created from it are unaffected. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/`` + + Args: + template_uuid: Report template UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/", + cast_to=None, + path_params={"template_uuid": template_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + template_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + field_config: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReportTemplateDetail: + """Update report template. + + Partially update a report template. Re-validates obligatory goAML fields on save. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/`` + + Args: + template_uuid: Report template UUID. + name + jurisdiction: ISO 3166-1 alpha-2 country code, upper-cased on save. + report_type + field_config: Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ReportTemplateDetail", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/", + cast_to=ReportTemplateDetail, + path_params={"template_uuid": template_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "jurisdiction": jurisdiction, + "report_type": report_type, + "field_config": field_config, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncReportTemplates(AsyncAPIResource): + """The ``report_templates`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncReportTemplatesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncReportTemplatesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncReportTemplatesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncReportTemplatesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[ReportTemplateDetail]: + """List report templates. + + List FIU report templates for the application. + + ``GET /v3/organization/{organization_id}/application/{application_id}/report-templates/`` + + Args: + limit: Page size. + offset: Zero-based offset. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[ReportTemplateDetail]", + self._client.request_page( + "get", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/", + cast_to=ReportTemplatesListResponse, + item_type=ReportTemplateDetail, + scope="application", + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + name: str, + jurisdiction: str, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str, + field_config: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReportTemplateDetail: + """Create report template. + + Create a reusable per-jurisdiction, per-report-type goAML header template. Rejected with 400 if any of the template-level obligatory fields (rentity_id, submission_code, currency_code_local) is missing from field_config; an invalid template cannot be saved or used to create a report. Per-filing header fields (action, submission_date, transactions) are enforced at report validate/finalize time instead. + + ``POST /v3/organization/{organization_id}/application/{application_id}/report-templates/`` + + Args: + name + jurisdiction: ISO 3166-1 alpha-2 country code, upper-cased on save. + report_type + field_config: Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ReportTemplateDetail", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/", + cast_to=ReportTemplateDetail, + scope="application", + auth="api_key", + body={ + "name": name, + "jurisdiction": jurisdiction, + "report_type": report_type, + "field_config": field_config, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReportTemplateDetail: + """Get report template. + + Retrieve a report template. + + ``GET /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/`` + + Args: + template_uuid: Report template UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ReportTemplateDetail", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/", + cast_to=ReportTemplateDetail, + path_params={"template_uuid": template_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + template_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete report template. + + Delete a report template. Reports already created from it are unaffected. + + ``DELETE /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/`` + + Args: + template_uuid: Report template UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/", + cast_to=None, + path_params={"template_uuid": template_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + template_uuid: str, + *, + name: str | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] + | str + | NotGiven = NOT_GIVEN, + field_config: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ReportTemplateDetail: + """Update report template. + + Partially update a report template. Re-validates obligatory goAML fields on save. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/`` + + Args: + template_uuid: Report template UUID. + name + jurisdiction: ISO 3166-1 alpha-2 country code, upper-cased on save. + report_type + field_config: Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "ReportTemplateDetail", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/", + cast_to=ReportTemplateDetail, + path_params={"template_uuid": template_uuid}, + scope="application", + auth="api_key", + body={ + "name": name, + "jurisdiction": jurisdiction, + "report_type": report_type, + "field_config": field_config, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class ReportTemplatesWithRawResponse: + def __init__(self, resource: ReportTemplates) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncReportTemplatesWithRawResponse: + def __init__(self, resource: AsyncReportTemplates) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + + +class ReportTemplatesWithStreamingResponse: + def __init__(self, resource: ReportTemplates) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncReportTemplatesWithStreamingResponse: + def __init__(self, resource: AsyncReportTemplates) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/sessions/__init__.py b/src/didit/resources/sessions/__init__.py new file mode 100644 index 0000000..76d7075 --- /dev/null +++ b/src/didit/resources/sessions/__init__.py @@ -0,0 +1,3074 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``sessions`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.sessions.batch_delete_response import ( + SessionsBatchDeleteResponse, + SessionsBatchDeleteResponse202, +) +from didit.types.sessions.bulk_update_aml_hit_status_params import ( + SessionsBulkUpdateAmlHitStatusParamsHitUpdates, +) +from didit.types.sessions.create_params import ( + SessionsCreateParamsContactDetails, + SessionsCreateParamsExpectedDetails, +) +from didit.types.sessions.create_response import SessionsCreateResponse +from didit.types.sessions.delete_response import SessionsDeleteResponse +from didit.types.sessions.list_networks_response import SessionsListNetworksResponse +from didit.types.sessions.list_response import ( + SessionsListResponse, + SessionsListResponseResultsKybSessionRowSessionKindBusiness, + SessionsListResponseResultsKycSessionRowSessionKindUser, +) +from didit.types.sessions.preview_resubmission_params import ( + SessionsPreviewResubmissionParamsNodesToResubmit, +) +from didit.types.sessions.preview_resubmission_response import SessionsPreviewResubmissionResponse +from didit.types.sessions.retrieve_decision_response import SessionsRetrieveDecisionResponse +from didit.types.sessions.retrieve_deletion_job_response import SessionsRetrieveDeletionJobResponse +from didit.types.sessions.share_response import SessionsShareResponse +from didit.types.sessions.update_data_params import SessionsUpdateDataParamsParsedAddress +from didit.types.sessions.update_data_response import SessionsUpdateDataResponse +from didit.types.sessions.update_feature_status_response import SessionsUpdateFeatureStatusResponse +from didit.types.sessions.update_kyb_company_data_response import ( + SessionsUpdateKybCompanyDataResponse, +) +from didit.types.sessions.update_kyb_party_response import SessionsUpdateKybPartyResponse +from didit.types.sessions.update_poa_data_params import SessionsUpdatePoaDataParamsPoaParsedAddress +from didit.types.sessions.update_poa_data_response import SessionsUpdatePoaDataResponse +from didit.types.sessions.update_status_params import SessionsUpdateStatusParamsNodesToResubmit +from didit.types.sessions.update_status_response import SessionsUpdateStatusResponse + +if TYPE_CHECKING: + from didit.resources.sessions.imports import ( + AsyncSessionsImports, + AsyncSessionsImportsWithRawResponse, + AsyncSessionsImportsWithStreamingResponse, + SessionsImports, + SessionsImportsWithRawResponse, + SessionsImportsWithStreamingResponse, + ) + from didit.resources.sessions.reviews import ( + AsyncSessionsReviews, + AsyncSessionsReviewsWithRawResponse, + AsyncSessionsReviewsWithStreamingResponse, + SessionsReviews, + SessionsReviewsWithRawResponse, + SessionsReviewsWithStreamingResponse, + ) + +__all__ = [ + "Sessions", + "AsyncSessions", + "SessionsWithRawResponse", + "AsyncSessionsWithRawResponse", + "SessionsWithStreamingResponse", + "AsyncSessionsWithStreamingResponse", +] + + +class Sessions(SyncAPIResource): + """The ``sessions`` resource of the Didit API. + + Methods: ``batch_delete``, ``bulk_update_aml_hit_status``, ``create``, ``delete``, ``generate_pdf``, ``import_shared``, ``list``, ``list_networks``, ``preview_resubmission``, ``retrieve_decision``, ``retrieve_deletion_job``, ``share``, ``update_aml_hit_status``, ``update_data``, ``update_feature_status``, ``update_kyb_company_data``, ``update_kyb_party``, ``update_poa_data``, ``update_status``. + + Nested resources: ``imports``, ``reviews``. + """ + + @cached_property + def imports(self) -> SessionsImports: + from didit.resources.sessions.imports import SessionsImports + + return SessionsImports(self._client) + + @cached_property + def reviews(self) -> SessionsReviews: + from didit.resources.sessions.reviews import SessionsReviews + + return SessionsReviews(self._client) + + @cached_property + def with_raw_response(self) -> SessionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return SessionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return SessionsWithStreamingResponse(self) + + def create( + self, + *, + workflow_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + callback: str | NotGiven = NOT_GIVEN, + callback_method: Literal["initiator", "completer", "both"] | str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + language: Literal[ + "en", + "ar", + "bg", + "bn", + "bs", + "ca", + "cnr", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh", + ] + | str + | NotGiven = NOT_GIVEN, + contact_details: SessionsCreateParamsContactDetails | NotGiven = NOT_GIVEN, + expected_details: SessionsCreateParamsExpectedDetails | NotGiven = NOT_GIVEN, + portrait_image: str | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsCreateResponse: + """Create a verification session. + + Create a User Verification (KYC) or Business Verification (KYB) session and receive a hosted verification `url` plus a `session_token` to redirect or embed for your end user. The `workflow_id` selects which verification steps run and whether the session is KYC or KYB. + + Call this from your backend (never the browser - the API key is a secret) whenever a user or business needs to be verified: at signup, before a sensitive action, or when re-verification is required. Prerequisites: a published workflow (created in the Console [Workflows](https://docs.didit.me/console/workflows) page) and an application API key with available credits. Sandbox applications bypass the credit check. + + **Idempotency:** when `vendor_data` is provided and an unfinished session (`Not Started`, `In Progress`, `Resubmitted`, or `Awaiting User`) with the same `vendor_data` already exists on the workflow's **current latest published version** (and that session already has a hosted verification URL), the existing session is returned (still `201`) instead of creating a duplicate - with its `callback` and `metadata` updated to the newly provided values. An unfinished session created on an older version of the workflow (one that has since been republished) is **not** reused; a new session pinned to the latest published version is created instead. Sessions in `Approved`, `Declined`, `In Review`, `Expired`, `Abandoned`, or `Kyc Expired` are never reused. + + **Side effects:** the hosted verification URL is generated and stored; if `contact_details.send_notification_emails` is `true` and an email is provided, a verification invite email is sent. The session expires after the workflow's configured session expiration time. For KYB workflows, `contact_details.phone` and `expected_details` are ignored. + + ``POST /v3/session/`` + + Args: + workflow_id: Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB. + vendor_data: A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection. + callback: URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. + callback_method: Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`. + metadata: Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context. + language: Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages). + contact_details: User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number. + expected_details: Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored. + portrait_image: Base64-encoded portrait image of the end user's face (max 2MB; JPEG, PNG, WebP, or TIFF). Used as the reference image to match against the liveness capture when the workflow is a `Biometric Authentication` workflow with Face Match enabled, or any graph workflow where Face Match runs before ID Verification (OCR). Optional when the `vendor_data` user already has a stored face image: Didit reuses it automatically, resolved in priority order: approved liveness face, ePassport chip photo, ID document portrait, manually enrolled profile face (only faces from approved sessions or faces you enrolled yourself are reused). When provided, `portrait_image` always takes precedence over the stored face. If omitted and no stored face exists (or `vendor_data` is not sent), session creation fails with `400`. Ignored for other workflow types. + sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsCreateResponse", + self._client.request( + "post", + "/v3/session/", + cast_to=SessionsCreateResponse, + auth="api_key", + body={ + "workflow_id": workflow_id, + "vendor_data": vendor_data, + "callback": callback, + "callback_method": callback_method, + "metadata": metadata, + "language": language, + "contact_details": contact_details, + "expected_details": expected_details, + "portrait_image": portrait_image, + "sandbox_scenario": sandbox_scenario, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_decision( + self, + session_id: str, + *, + include: Literal["events", "resubmissions", "events,resubmissions"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsRetrieveDecisionResponse: + """Retrieve the full decision for a verification session. + + Retrieve the complete decision report for a verification session - the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`"user"` or `"business"`). + + **Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` - never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. + + **When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works - the endpoint can be called at any point in the session lifecycle and returns the data produced so far - but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references. + + **Statuses.** The top-level `status` is the session lifecycle status (`Not Started`, `In Progress`, `Awaiting User`, `In Review`, `Approved`, `Declined`, `Resubmitted`, `Expired`, `Kyc Expired`, `Abandoned`). Each feature item additionally carries its own feature-level `status` (`Not Finished`, `Approved`, `Declined`, `In Review`, ...). A session can be `Approved` overall while an individual feature is `In Review`, depending on the workflow's decision rules. + + **Authentication and visibility.** Authenticate with the application API key in the `x-api-key` header (a console user Bearer token with the `read:sessions` permission also works). Only sessions owned by the authenticated application are visible. If your workflow restricts response attributes, non-allowed fields inside feature items are returned as `null` while `status`, `warnings`, and `node_id` are always preserved. + + **Rate limits.** GET requests are limited to 600 per minute per API key. Exceeding the limit returns `429` with `Retry-After` and `X-RateLimit-*` headers. + + ``GET /v3/session/{sessionId}/decision/`` + + Args: + session_id: UUID of the verification session whose decision you want to retrieve. This is the `session_id` returned by `POST /v3/session/`. The same path works for both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the id against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. Validate the UUID format client-side so you can distinguish an input error from a genuine missing-session `404`. + include: Comma-separated list of extra payload sections. Supported values: `events` and `resubmissions`, alone or together (`events,resubmissions`). When `include=events` is passed, the response additionally contains the session event timeline (`events`, including the `WORKFLOW_BRANCH_EVALUATED` / `WORKFLOW_STATUS_NODE_REACHED` routing events described on the Retrieve Session page), the full activity log (`activity_reviews`), blocklist flags (`blocklisted`), a per-session `cost_breakdown`, workflow metadata (`workflow_type`, `workflow_version`, `workflow_version_id`, and `workflow_terminal_node_id` - the last recorded workflow status node that set a session status, or `null` if none has been recorded; historical provenance that is not cleared by manual approvals, declines or resubmissions, and is replaced by a later workflow status node), and session metadata (`api_service` - string or `null`, set only for standalone-API sessions; `session_type` - `API`, `HOSTED`, or `MIGRATED`; `has_device_nfc_support` - boolean). Two fields change relative to the plain shape: the top-level `session_kind` discriminator is **omitted** (the events shape only exists for user sessions), and the `features` array switches from plain strings to `{feature, node_id}` objects - where the email feature is reported as `EMAIL` instead of `EMAIL_VERIFICATION`. This expanded shape is primarily used by the Didit Console; most API integrations should omit the parameter. **KYB limitation:** `include=events` only applies to user (KYC) sessions. Business sessions (`session_kind = "business"`) always return the standard business decision shape - `events`, `activity_reviews`, `blocklisted`, `cost_breakdown` and the extra `workflow_*`/session metadata are never returned for business sessions, and their `features` array stays a plain string array. With `include=resubmissions` (user and business sessions alike) the response also contains `resubmissions`: one entry per resubmission request, oldest first, each with `number`, `created_at`, `previous_status`, `nodes`, `reasons`, `requested_by` and `results` (what the resubmitted steps returned before the request, under the same keys and in the same shape as the current results). Each `cost_breakdown` item carries `usage_type`, `price`, `details` and `created_at`: a resubmitted step that runs again is billed as a new item. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsRetrieveDecisionResponse", + self._client.request( + "get", + "/v3/session/{sessionId}/decision/", + cast_to=SessionsRetrieveDecisionResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"include": include}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + session_id: str, + *, + retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, + face_retention_days: int | NotGiven = NOT_GIVEN, + face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] + | str + | NotGiven = NOT_GIVEN, + instruction_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsDeleteResponse: + """Delete a verification session (KYC or KYB). + + Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL. + + **What is deleted:** + - The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log. + - All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Previously issued media URLs (`https:///...`) stop resolving. + - The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**). + - Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected. + - For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted. + + The session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop. + + **What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{"share_token": ["Original session does not exist."]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it. + + **Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one. + + **Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it. + + **Side effects:** no webhook is emitted for deletions. + + **Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log. + + **Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it. + + **Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`. + + **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + To bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead. + + ``DELETE /v3/session/{sessionId}/delete/`` + + Args: + session_id: UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID - a non-UUID value does not match the route and returns `404`. + retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. + face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. + face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. + deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). + instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsDeleteResponse", + self._client.request( + "delete", + "/v3/session/{sessionId}/delete/", + cast_to=SessionsDeleteResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "retain_face_embeddings": retain_face_embeddings, + "face_retention_days": face_retention_days, + "face_retention_deadline": face_retention_deadline, + "deletion_instruction": deletion_instruction, + "instruction_id": instruction_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def generate_pdf( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a PDF report for a verification session. + + Render and download a compliance-ready PDF report for a finished verification session. The same path serves both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the `sessionId` against both session types and picks the matching report template. + + **Eligible statuses.** The session must already carry a final or reviewable status: + + - KYC sessions: `Approved`, `Declined`, `In Review`, or `Kyc Expired`. + - KYB sessions: `Approved`, `Declined`, or `In Review`. + + Any other status (`Not Started`, `In Progress`, `Expired`, `Abandoned`, …) returns `403` with an explanatory `detail` string - see the `403` response below. + + **What the report contains.** The PDF mirrors the console session view at the moment of the request: + + - **KYC** - rolled-up session status, the warnings list, and one section per executed feature: ID Verification (document images plus extracted fields), NFC, Liveness, Face Match, Email Verification, Phone Verification, Proof of Address, Questionnaire answers, Database Validation, AML Screening, and IP Analysis - followed by session tags, console review activity (comments and status changes), and the session event timeline. + - **KYB** - registry checks (company data), AML screenings, business document verifications, key-people checks, questionnaire responses, phone/email verifications, IP analyses, and the session event timeline, followed by tags and review activity. + + **Branding.** If your application has white-label customization, the report is rendered with your logo and links to your privacy-policy URL; otherwise it carries Didit branding. Reports are rendered in English - there is no language parameter. + + **No caching.** Every call re-renders the PDF from the current session data, so a report generated after a manual review reflects the reviewer’s decision. Two calls for the same session can produce byte-different files - archive the downloaded file if you need an immutable copy. + + **Latency.** Rendering downloads every stored image (document sides, selfies, extra files) before composing the PDF, so media-heavy sessions can take several seconds. Use a generous client read timeout (60 s recommended) and stream the body to disk. + + **Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response - there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples. + + If you need the underlying data as JSON instead of a rendered report, use `GET /v3/session/{sessionId}/decision/`. For every reportable session of one user in a single document, use `GET /v3/users/{vendor_data}/generate-pdf/`. + + ``GET /v3/session/{sessionId}/generate-pdf/`` + + Args: + session_id: UUID of the User Verification (KYC) or Business Verification (KYB) session to render - the `session_id` returned by `POST /v3/session/`. The same path works for both session kinds; use the `Content-Disposition` filename on the response to tell which report type you received. **Must be a well-formed, lowercase UUID.** The route only matches valid UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. The route's UUID converter only matches the canonical lowercase form - uppercase-hex UUIDs are rejected with the HTML 404. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/session/{sessionId}/generate-pdf/", + cast_to=bytes, + path_params={"sessionId": session_id}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_status( + self, + session_id: str, + *, + new_status: Literal["Approved", "Declined", "Resubmitted"] | str, + comment: str | NotGiven = NOT_GIVEN, + nodes_to_resubmit: Sequence[SessionsUpdateStatusParamsNodesToResubmit] + | NotGiven = NOT_GIVEN, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + email_preview_token: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateStatusResponse: + """Approve, decline, or request resubmission for a verification session. + + Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs. + + Call it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. The new status must differ from the current one, and your API key's organization needs the `write:sessions` permission. The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid. + + For `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected - list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. Captured data for the selected steps is deleted, the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly - resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically. + + Every successful call fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry - with actor attribution and your `comment` - to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. + + The operation is intentionally not repeatable with the same value: re-sending the current status returns `400` (`The new status is the same as the current status.`), so accidental double-submissions are harmless. Authentication and permission failures both return `403` - this API never responds `401`. + + ``PATCH /v3/session/{sessionId}/update-status/`` + + Args: + session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + new_status: Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`. + comment: Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`. + nodes_to_resubmit: Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) - features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` ("No features found that need resubmission") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` - those parent checks recompute from their child KYC sessions. + send_email: Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons. + email_address: Recipient for the notification email. **Required when `send_email` is `true`** - omitting it returns `400`. + email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). + email_preview_token: Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateStatusResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/update-status/", + cast_to=SessionsUpdateStatusResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "new_status": new_status, + "comment": comment, + "nodes_to_resubmit": nodes_to_resubmit, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + "email_preview_token": email_preview_token, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + *, + session_kind: Literal["user", "business", "all"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + country: str | NotGiven = NOT_GIVEN, + document_type: str | NotGiven = NOT_GIVEN, + document_name: str | NotGiven = NOT_GIVEN, + region: str | NotGiven = NOT_GIVEN, + didit_internal_id: str | NotGiven = NOT_GIVEN, + session_type: str | NotGiven = NOT_GIVEN, + api_service: Literal[ + "ID_VERIFICATION", + "FACE_MATCH", + "AGE_ESTIMATION", + "FACE_SEARCH", + "POA", + "AML", + "KYB_REGISTRY", + "PASSIVE_LIVENESS", + "DATABASE_VALIDATION", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + ] + | str + | NotGiven = NOT_GIVEN, + screened_full_name: str | NotGiven = NOT_GIVEN, + warning: str | NotGiven = NOT_GIVEN, + status_initialized_as_in_review: bool | NotGiven = NOT_GIVEN, + is_kyc_to_be_reviewed: bool | NotGiven = NOT_GIVEN, + euuid: str | NotGiven = NOT_GIVEN, + organization_name: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[ + SessionsListResponseResultsKycSessionRowSessionKindUser + | SessionsListResponseResultsKybSessionRowSessionKindBusiness + ]: + """List verification sessions (KYC and/or KYB) with filters and pagination. + + Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`). + + **Canonical URL:** the route is `/v3/sessions/` (trailing slash). Requests to `/v3/sessions` without the slash receive a `301` redirect to the slashed URL with the query string preserved; most HTTP clients follow it transparently on GET. + + **When to use this endpoint vs webhooks.** Use this endpoint for dashboards, back-office search, reconciliation, and batch exports. Do not poll it to track the progress of individual verifications - configure [webhooks](/integration/webhooks) instead, which push `status.updated` events the moment a session changes. For one session's full result, call `GET /v3/session/{sessionId}/decision/`. + + **KYC and KYB sessions.** `session_kind` selects which kinds are listed: + - `user` (default) - User Verification (KYC) sessions only. Backward-compatible behavior. + - `business` - Business Verification (KYB) sessions only. Rows have a different, smaller shape (see the response schema). + - `all` - both kinds in one response: the KYC page and the KYB page for the same `limit`/`offset` are concatenated in `results` (KYC rows first), so a page can contain up to `2 x limit` rows. `count` is the sum of both kinds and `next`/`previous` reflect the KYC-side pagination. For strict pagination of mixed data, call `user` and `business` separately. + + Every row carries a `session_kind` discriminator (`"user"` or `"business"`). + + **Pagination.** Standard limit/offset: `limit` (default 50) and `offset` (default 0), with `next`/`previous` ready-made page URLs. For KYC rows `count` is exact. For KYB rows `count` is computed with a bounded scan and is capped at 100 - when there are more than 100 matches it stays at 100, so iterate using `next` (or by checking whether a full page came back) rather than computing total pages from `count`. + + **Filter semantics.** All filters combine with AND and apply to KYC rows. When `session_kind` is `business` or `all`, the KYB side honors the shared parameters `vendor_data`, `status`, `workflow_id`, `country` (registry-company country code), `date_from`, `date_to`, `search` (company name, registration number - type registration numbers without separators, e.g. `DPC001` not `DPC-001` - vendor_data, session number, or session UUID), `warning`, and `organization_name`; the remaining KYC-only filters are ignored for KYB rows. Comma-separated parameters (`status`, `country`, `document_type`, `region`, `workflow_id`, `document_name`, `warning`, `organization_name`, `session_type`) match any of the listed values (OR within the parameter). `vendor_data` is an exact match - use `search` for partial matching. Unknown values in list filters simply return no rows (no error); malformed typed values (`date_from`, `date_to`, `didit_internal_id`, `api_service`, `session_kind`) return `400` with a per-field error object. + + **Search.** `search` adapts to the input: a full UUID looks up `session_id`; digits (with optional `+ - . space`) match session-number prefix, document/personal numbers, phone-number prefix, or `vendor_data`; values containing `@` match email prefixes; any other text matches names (accent-insensitive) and `vendor_data`. + + No fixed per-endpoint rate limit is enforced, but keep list polling infrequent and rely on webhooks for real-time updates. + + ``GET /v3/sessions/`` + + Args: + session_kind: Which kind of sessions to list: `user` (KYC only, default), `business` (KYB only), or `all` (both, concatenated - see the description for merged-pagination semantics). Matching is case-insensitive (`BUSINESS` works); any value other than a case variant of `user`/`business`/`all` returns `400`. + limit: Maximum number of rows per page (per kind when `session_kind=all`). Defaults to 50. + offset: Number of rows to skip before the first returned row. Defaults to 0. Prefer following the `next`/`previous` URLs from the response. + status: Comma-separated list of session statuses to include, e.g. `Approved,In Review`. Values are case-sensitive; see [Verification Statuses](/integration/verification-statuses). Unknown values match nothing. + vendor_data: Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead. + search: Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes. + workflow_id: Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty. + date_from: Only sessions created on or after this date (UTC), format `YYYY-MM-DD`. Malformed dates return `400`. + date_to: Only sessions created up to this date (UTC), format `YYYY-MM-DD`. For KYC rows the entire day is included; for KYB rows the comparison is `created_at <= date_to` (midnight), so use the next day's date to include a full day of KYB sessions. + country: Comma-separated country filter. For KYC rows: ISO 3166-1 alpha-3 codes of the ID document's issuing state (e.g. `ESP,USA`), which may differ from nationality. For KYB rows: the registry company's country code. + document_type: Comma-separated ID-document type codes verified in the session (KYC rows only): `P` (Passport), `ID` (Identity Card), `DL` (Driver's License), `RP` (Residence Permit), `SSC` (Social Security Card), `HIC` (Health Insurance Card), `WP` (Work Permit), `TC` (Tax Card), `VISA`, `PSC` (Public Service Card), `BC` (Birth Certificate), `OTHER`. Note the response field `document_type` contains the display name, not the code. + document_name: Exact document names; multiple names can be passed comma-separated, but the value is only split on commas when it contains more than one ` - ` separator (a lone name with an embedded comma is matched whole). Names are as recognized by document verification (front or back side), e.g. `Spain - Id Card (2021)`. KYC rows only. + region: Comma-separated document region/state values (for documents issued per region, such as US driver's licenses). KYC rows only. + didit_internal_id: Filter by Didit's internal user identifier (the `didit_internal_id` response field) to list all sessions of one verified user. Must be a valid UUID; malformed values return `400`. KYC rows only. + session_type: Comma-separated session creation channels: `API`, `HOSTED`, `MIGRATED`. The special value `ALL` (or omitting the parameter) disables the filter. KYC rows only. + api_service: Filter standalone API sessions by service. Invalid values return `400`. KYC rows only. + screened_full_name: Accent-insensitive name search inside AML and database-validation screened data. When combined with `api_service=AML` or `api_service=DATABASE_VALIDATION`, the search is restricted to that feature's screened data. KYC rows only. + warning: Comma-separated warning risk codes; returns sessions that raised any of them (e.g. `POSSIBLE_FRAUD`). Risk codes are listed per feature in the [warnings documentation](/integration/webhooks). Applies to KYC rows and, with business warning codes, to KYB rows. + status_initialized_as_in_review: When `true`, only sessions whose decision initially landed in `In Review` (useful to audit manual-review volume). KYC rows only. + is_kyc_to_be_reviewed: `true` returns sessions whose document verification is flagged for manual review; `false` returns sessions with document verification explicitly not flagged. Omit to disable. KYC rows only. + euuid: Filter by the end user's euuid (UUID assigned to the verifying user, used by Console deep links). Must be a valid UUID; malformed values return a 400 field error. KYC rows only. + organization_name: Comma-separated organization names. Console-oriented; with an API key (already scoped to one application) it only matches your own organization's name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[SessionsListResponseResultsKycSessionRowSessionKindUser | SessionsListResponseResultsKybSessionRowSessionKindBusiness]", + self._client.request_page( + "get", + "/v3/sessions/", + cast_to=SessionsListResponse, + item_type=SessionsListResponseResultsKycSessionRowSessionKindUser + | SessionsListResponseResultsKybSessionRowSessionKindBusiness, + auth="api_key", + query={ + "session_kind": session_kind, + "limit": limit, + "offset": offset, + "status": status, + "vendor_data": vendor_data, + "search": search, + "workflow_id": workflow_id, + "date_from": date_from, + "date_to": date_to, + "country": country, + "document_type": document_type, + "document_name": document_name, + "region": region, + "didit_internal_id": didit_internal_id, + "session_type": session_type, + "api_service": api_service, + "screened_full_name": screened_full_name, + "warning": warning, + "status_initialized_as_in_review": status_initialized_as_in_review, + "is_kyc_to_be_reviewed": is_kyc_to_be_reviewed, + "euuid": euuid, + "organization_name": organization_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def share( + self, + session_id: str, + *, + for_application_id: str, + ttl_in_seconds: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsShareResponse: + """Mint a share token for a finished verification session. + + Mint a short-lived JWT that lets a specific Didit application import this finished session. Pair with `POST /v3/session/import-shared/`. + + ``POST /v3/session/{sessionId}/share/`` + + Args: + session_id: UUID of the source verification session to mint a token for. + for_application_id: UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**. + ttl_in_seconds: Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsShareResponse", + self._client.request( + "post", + "/v3/session/{sessionId}/share/", + cast_to=SessionsShareResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={"for_application_id": for_application_id, "ttl_in_seconds": ttl_in_seconds}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def import_shared( + self, + *, + share_token: str, + trust_review: bool, + workflow_id: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Redeem a share token to clone a verification session into your application. + + Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent - a token redeems once per receiver. + + ``POST /v3/session/import-shared/`` + + Args: + share_token: JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session). + trust_review: If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`. + workflow_id: UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`. + vendor_data: Optional override for the cloned session's `vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + self._client.request( + "post", + "/v3/session/import-shared/", + cast_to=object, + auth="api_key", + body={ + "share_token": share_token, + "trust_review": trust_review, + "workflow_id": workflow_id, + "vendor_data": vendor_data, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def batch_delete( + self, + *, + session_numbers: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, + face_retention_days: int | NotGiven = NOT_GIVEN, + face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] + | str + | NotGiven = NOT_GIVEN, + instruction_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202: + """Bulk delete KYC sessions by session_number. + + Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`. + + `session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead. + + **Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`: + - Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`. + - More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted. + + Each deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible. + + **Selection semantics:** + - Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`. + - Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing. + - Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself. + - There is no documented cap on the list length; the shared write rate limit (below) is the practical bound. + + **Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`. + + **Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. + + **Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped. + + **`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo. + + **Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`); this API never returns `401`. + + **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + This endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both). + + ``POST /v3/sessions/delete/`` + + Args: + session_numbers: Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped. + delete_all: When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step. + retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. + face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. + face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. + deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). + instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202", + self._client.request( + "post", + "/v3/sessions/delete/", + cast_to={200: SessionsBatchDeleteResponse, 202: SessionsBatchDeleteResponse202}, + auth="api_key", + body={ + "session_numbers": session_numbers, + "delete_all": delete_all, + "retain_face_embeddings": retain_face_embeddings, + "face_retention_days": face_retention_days, + "face_retention_deadline": face_retention_deadline, + "deletion_instruction": deletion_instruction, + "instruction_id": instruction_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_deletion_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsRetrieveDeletionJobResponse: + """Get a bulk session deletion job. + + Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`. + + The job reports how many sessions ended in each outcome and how many it has processed, never which sessions it deleted. A `FAILED` job carries the reason in `error`; the sessions it had not reached are still in place, so repeat the deletion request to delete them. + + **Authentication:** API key in the `x-api-key` header. A job is only visible to the application that created it; any other `job_id` returns `404`. Missing or invalid credentials return `403`; this API never returns `401`. + + ``GET /v3/sessions/delete/{job_id}/`` + + Args: + job_id: The `job_id` returned with the `202` from `POST /v3/sessions/delete/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsRetrieveDeletionJobResponse", + self._client.request( + "get", + "/v3/sessions/delete/{job_id}/", + cast_to=SessionsRetrieveDeletionJobResponse, + path_params={"job_id": job_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_data( + self, + session_id: str, + *, + document_type: Literal[ + "P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER" + ] + | str + | None + | NotGiven = NOT_GIVEN, + document_subtype: str | None | NotGiven = NOT_GIVEN, + document_number: str | None | NotGiven = NOT_GIVEN, + personal_number: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + date_of_issue: str | _dt.date | None | NotGiven = NOT_GIVEN, + expiration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + issuing_state: str | None | NotGiven = NOT_GIVEN, + first_name: str | None | NotGiven = NOT_GIVEN, + last_name: str | None | NotGiven = NOT_GIVEN, + gender: Literal["M", "F", "U"] | str | None | NotGiven = NOT_GIVEN, + address: str | None | NotGiven = NOT_GIVEN, + place_of_birth: str | None | NotGiven = NOT_GIVEN, + nationality: str | None | NotGiven = NOT_GIVEN, + marital_status: Literal["SINGLE", "MARRIED", "DIVORCED", "WIDOWED", "UNKNOWN"] + | str + | NotGiven = NOT_GIVEN, + extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + parsed_address: SessionsUpdateDataParamsParsedAddress | None | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateDataResponse: + """Update KYC data extracted from the ID document on a verification session. + + Correct the identity fields that were extracted (OCR / NFC) from the user's ID document - misread names, dates, document numbers, and so on - after the verification has finished. Typical callers are a compliance analyst working from your own back office, or an automated reconciliation job. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). + + The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry an ID-verification (KYC) record: sessions whose workflow never ran an ID-verification step return `404` (`"The session does not have a KYC."`). For graph workflows with multiple ID-verification steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent KYC record is patched. + + Two derived fields are recomputed on every call: `full_name` (from `first_name` + `last_name`) and `issuing_state_name` (from `issuing_state`). If the free-text `address` changes, the server may also re-geocode it and refresh `parsed_address` (skipped for sandbox sessions). `extra_fields` and `parsed_address` are merged key-by-key rather than replaced - see the per-field descriptions for the `null`-to-delete semantics. + + Submitting at least one valid field fires the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) - even when the submitted values equal the stored ones. When values actually change, a `KYC_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. An empty JSON body (`{}`) is accepted and simply returns the current KYC data without firing the webhook. + + Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + + ``PATCH /v3/session/{sessionId}/update-data/`` + + Args: + session_id: UUID of the User Verification (KYC) session whose extracted document data you are correcting. Business (KYB) session IDs are not valid here - for company data use [Update KYB Company Data](#patch-/v3/session/-sessionId-/kyb/-companyUuid-/update-data/). + document_type: Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`. + document_subtype: Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants. + document_number: Document number as printed on the ID. + personal_number: Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport). + date_of_birth: Date of birth in `YYYY-MM-DD`. Any other format returns `400`. + date_of_issue: Issue date of the document in `YYYY-MM-DD`. + expiration_date: Expiration date of the document in `YYYY-MM-DD`. + issuing_state: Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`. + first_name: Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint. + last_name: Family name(s). Also feeds the recomputed `full_name`. + gender: Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value - including `X` - returns `400`. + address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding - the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record. + place_of_birth: Place of birth as printed on the document. + nationality: Nationality as printed on the document. Free text up to 32 characters - typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one. + marital_status: Marital status where the document carries it. Any other value returns `400`. + extra_fields: Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `""` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied). + parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response - you cannot set it directly. + node_id: Workflow graph node whose KYC record to patch, for graph workflows with multiple ID-verification steps (also accepted as a `node_id` body field). When omitted, the most recent KYC record on the session is patched. If no KYC exists for the given node, the call returns `404` (`"The session does not have a KYC for node ''."`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateDataResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/update-data/", + cast_to=SessionsUpdateDataResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id}, + body={ + "document_type": document_type, + "document_subtype": document_subtype, + "document_number": document_number, + "personal_number": personal_number, + "date_of_birth": date_of_birth, + "date_of_issue": date_of_issue, + "expiration_date": expiration_date, + "issuing_state": issuing_state, + "first_name": first_name, + "last_name": last_name, + "gender": gender, + "address": address, + "place_of_birth": place_of_birth, + "nationality": nationality, + "marital_status": marital_status, + "extra_fields": extra_fields, + "parsed_address": parsed_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_feature_status( + self, + session_id: str, + node_id: str, + *, + new_status: Literal["Approved", "Declined", "In Review"] | str, + comment: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateFeatureStatusResponse: + """Update an individual feature (step) status. + + Set the status of a single verification feature (step) within a session **without changing the overall session decision**. Target the feature by its workflow graph `nodeId` (use `default` for non-graph workflows where the step has no node). + + Works for both User Verification (KYC) and Business Verification (KYB) sessions and every step type - ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. + + `new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. This endpoint is **not** available for standalone API sessions (`session_type = API`). + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + node_id: Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID. + new_status: Target status for this feature. Must differ from the current value. + comment: Optional free-text note recorded on the session's review trail for this feature change. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateFeatureStatusResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/features/{nodeId}/update-status/", + cast_to=SessionsUpdateFeatureStatusResponse, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + body={"new_status": new_status, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_aml_hit_status( + self, + session_id: str, + *, + hit_id: str, + review_status: Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] + | str, + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Update an AML hit review status. + + Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array - see the session decision). + + `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. Reviewer decisions are preserved across ongoing-monitoring updates, so a hit you mark `False Positive` stays that way when the provider re-screens. + + A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/update-aml-hit-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + hit_id: The `id` of the hit in the AML check's `hits` array. + review_status: The new review status for the hit. + node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. + node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + self._client.request( + "patch", + "/v3/session/{sessionId}/update-aml-hit-status/", + cast_to=object, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id_query}, + body={"hit_id": hit_id, "review_status": review_status, "node_id": node_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def bulk_update_aml_hit_status( + self, + session_id: str, + *, + hit_updates: Sequence[SessionsBulkUpdateAmlHitStatusParamsHitUpdates], + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Bulk update AML hit review statuses. + + Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. + + The update is all-or-nothing: if any `hit_id` is not found in the AML check, the request returns `404` and no hit is changed. A successful call fires a single `data.updated` webhook and records one activity entry covering all changes. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/bulk-update-aml-hit-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + hit_updates: Non-empty list of hit updates. + node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. + node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + self._client.request( + "patch", + "/v3/session/{sessionId}/bulk-update-aml-hit-status/", + cast_to=object, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id_query}, + body={"hit_updates": hit_updates, "node_id": node_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_poa_data( + self, + session_id: str, + *, + issuing_state: str | None | NotGiven = NOT_GIVEN, + document_type: Literal[ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN", + ] + | str + | NotGiven = NOT_GIVEN, + document_language: str | None | NotGiven = NOT_GIVEN, + issuer: str | None | NotGiven = NOT_GIVEN, + issue_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + poa_address: str | None | NotGiven = NOT_GIVEN, + name_on_document: str | None | NotGiven = NOT_GIVEN, + extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + poa_parsed_address: SessionsUpdatePoaDataParamsPoaParsedAddress + | None + | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdatePoaDataResponse: + """Update Proof of Address data extracted from the POA document on a verification session. + + Correct the fields extracted from the user's Proof of Address document - utility bill, bank statement, or other address evidence - after the verification has finished: issuer, dates, the printed name, the address itself, and banking details. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). + + The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry a POA record: sessions whose workflow never ran a Proof of Address step return `404` (`"The session does not have a POA."`). For graph workflows with multiple POA steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent POA record is patched. + + `extra_fields` is merged key-by-key: nine known keys (`bank_iban`, `additional_names`, …) write through to dedicated POA columns, any other key is kept as a custom extra, and `null` / `""` deletes a key. `poa_parsed_address` accepts only the seven documented keys and recomputes `formatted_address` / `poa_formatted_address` after every edit. If the free-text `poa_address` changes, the server may also re-geocode it and refresh the structured address (skipped for sandbox sessions). + + Unlike the KYC variant of this endpoint, the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) fires only when at least one stored value **actually changed** - re-sending identical values is a silent no-op. When values change, a `POA_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. + + Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + + An empty JSON body `{}` is a no-op: it returns `200` echoing the current POA record without firing the webhook (`has_changes` stays false). + + ``PATCH /v3/session/{sessionId}/update-poa-data/`` + + Args: + session_id: UUID of the User Verification (KYC) session whose attached POA record you are correcting. + issuing_state: Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`. + document_type: POA document category. Values are uppercase - lowercase variants like `utility_bill` return `400`. + document_language: Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text - not validated as a language code. + issuer: Organization that issued the document (utility company, bank, government agency). + issue_date: Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`. + poa_address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record. + name_on_document: Recipient name as printed on the document. + extra_fields: Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `""` deletes it. Must be a JSON object, otherwise `400`. + poa_parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` - you cannot set it directly. + node_id: Workflow graph node whose POA record to patch, for graph workflows with multiple Proof of Address steps (also accepted as a `node_id` body field). When omitted, the most recent POA record on the session is patched. If no POA exists for the given node, the call returns `404` (`"The session does not have a POA for node ''."`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdatePoaDataResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/update-poa-data/", + cast_to=SessionsUpdatePoaDataResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id}, + body={ + "issuing_state": issuing_state, + "document_type": document_type, + "document_language": document_language, + "issuer": issuer, + "issue_date": issue_date, + "poa_address": poa_address, + "name_on_document": name_on_document, + "extra_fields": extra_fields, + "poa_parsed_address": poa_parsed_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_kyb_company_data( + self, + session_id: str, + company_uuid: str, + *, + company_name: str | NotGiven = NOT_GIVEN, + registration_number: str | NotGiven = NOT_GIVEN, + country_code: str | NotGiven = NOT_GIVEN, + region: str | None | NotGiven = NOT_GIVEN, + incorporation_date: str | _dt.date | NotGiven = NOT_GIVEN, + registration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + dissolution_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + tax_number: str | NotGiven = NOT_GIVEN, + legal_address: str | NotGiven = NOT_GIVEN, + company_type: str | NotGiven = NOT_GIVEN, + registry_status: Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + | NotGiven = NOT_GIVEN, + verification_status: Literal["verified", "failed", "unknown"] + | str + | None + | NotGiven = NOT_GIVEN, + alternative_names: str | NotGiven = NOT_GIVEN, + nature_of_business: str | NotGiven = NOT_GIVEN, + registered_capital: str | NotGiven = NOT_GIVEN, + registered_capital_amount: float | None | NotGiven = NOT_GIVEN, + registered_capital_currency: str | None | NotGiven = NOT_GIVEN, + website: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + phone: str | NotGiven = NOT_GIVEN, + legal_entity_identifier: str | NotGiven = NOT_GIVEN, + location_of_registration: str | NotGiven = NOT_GIVEN, + vat_number: str | NotGiven = NOT_GIVEN, + control_scheme: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateKybCompanyDataResponse: + """Update registry-extracted company data on a Business Verification (KYB) session. + + Correct the registry-extracted company data on a Business Verification (KYB) session - company name, registration number, addresses, status, contact details - identified by `companyUuid`. Typical callers are a compliance analyst fixing a registry misread, or your back office syncing corrected company data. Every field in the body is optional (`PATCH` semantics); an empty JSON body (`{}`) is a no-op that simply echoes the current company record. + + Edits are written in two places at once: fields with a canonical company column (name, registration number, country, incorporation date, tax number, type, statuses, capital fields (`registered_capital`, `registered_capital_amount`, `registered_capital_currency`), alternative names, nature of business, website, email, phone, LEI, location, and `legal_address` → `registered_address`) update that column, and **every** submitted key is also merged into the `registry_data` snapshot (dates as ISO strings) so the "extracted from registry" view reflects the correction. Fields without a column - `region`, `registration_date`, `dissolution_date`, `control_scheme` - land in `registry_data` only. `user_provided_data` (what the end user confirmed in the hosted flow) is never touched, and `last_console_edit_at` is stamped on every non-empty PATCH. + + Unlike the KYC/POA update endpoints, there is **no session-status gate**: the company can be edited at any point in the business session's lifecycle. The company must belong to the session in the path (`404` with `"KYB company does not belong to the given session."` otherwise) and to your application. Every non-empty PATCH fires the `data.updated` [webhook](/integration/webhooks) on the business session (trigger `console_registry_edit`) - even when the submitted values equal the stored ones. The session's status and decision are **not** re-evaluated, and no review/audit entry is recorded. + + Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. The shared write budget of 300 requests per minute per API key applies. + + For User Verification sessions, the KYC counterpart is `PATCH /v3/session/{sessionId}/update-data/`; to change the session decision itself use `PATCH /v3/session/{sessionId}/update-status/`. + + ``PATCH /v3/session/{sessionId}/kyb/{companyUuid}/update-data/`` + + Args: + session_id: UUID of the parent Business Verification (KYB) session. + company_uuid: UUID of the company record to update (`registry_checks[].company.uuid` on the business session decision). Must belong to the session referenced by `sessionId`. + company_name: Legal name of the company. Written to the canonical column and the registry snapshot. + registration_number: Official registration number with the company registry. + country_code: Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`. + region: Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only - it has no canonical column. + incorporation_date: Incorporation date in `YYYY-MM-DD`. Optional like every other field - the `PATCH` is partial. Any other date format returns `400`. + registration_date: Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only. + dissolution_date: Dissolution date for companies that are no longer active. Stored in the registry snapshot only. + tax_number: Tax identification number. + legal_address: Legal / registered address. Aliased server-side to the canonical `registered_address` column - the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent. + company_type: Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`). + registry_status: Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`. + verification_status: Outcome of the registry verification. Any other value returns `400`. + alternative_names: Trade names, former legal names, DBA names (free text). + nature_of_business: Short description of the company's activity. + registered_capital: Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text. + registered_capital_amount: Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `"1500000.50"`). + registered_capital_currency: ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`. + website: Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`. + email: Contact email on file with the registry. + phone: Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total - otherwise `400`. + legal_entity_identifier: LEI, where the company has one. + location_of_registration: City / subdivision of the registry of record. + vat_number: EU VAT number. Merged into the `registry_data` snapshot only - editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged. + control_scheme: Control / governance scheme when the registry exposes it. Stored in the registry snapshot only. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateKybCompanyDataResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/kyb/{companyUuid}/update-data/", + cast_to=SessionsUpdateKybCompanyDataResponse, + path_params={"sessionId": session_id, "companyUuid": company_uuid}, + auth="api_key", + body={ + "company_name": company_name, + "registration_number": registration_number, + "country_code": country_code, + "region": region, + "incorporation_date": incorporation_date, + "registration_date": registration_date, + "dissolution_date": dissolution_date, + "tax_number": tax_number, + "legal_address": legal_address, + "company_type": company_type, + "registry_status": registry_status, + "verification_status": verification_status, + "alternative_names": alternative_names, + "nature_of_business": nature_of_business, + "registered_capital": registered_capital, + "registered_capital_amount": registered_capital_amount, + "registered_capital_currency": registered_capital_currency, + "website": website, + "email": email, + "phone": phone, + "legal_entity_identifier": legal_entity_identifier, + "location_of_registration": location_of_registration, + "vat_number": vat_number, + "control_scheme": control_scheme, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_kyb_party( + self, + session_id: str, + party_uuid: str, + *, + is_skipped: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateKybPartyResponse: + """Skip (or re-require) a Key People party on a Business Verification (KYB) session. + + Exempt a Key People party from verification, or put it back in scope. This is the operator escape hatch for a party that can never finish its own check - most often because the individual KYC session handed to a UBO or director was deleted, which freezes that child's status and leaves the KYB parked in `Awaiting User` with nothing left that could move it. + + Setting `is_skipped: true` drops the party out of the Key People aggregation and immediately recomputes the `KYBKeyPeopleCheck` and the parent session status, so the business session can settle and then be approved or declined. Sending `is_skipped: false` puts the party back in scope and the check goes back to waiting on it. + + Unlike the applicant-facing skip in the hosted Key People screen, this endpoint is **not** gated by the role's `allow_skip` setting - that flag limits what the applicant may decide, not what your analyst may override. `requires_verification` is deliberately left untouched, so an overridden party reads as `requires_verification: true` **and** `is_skipped: true` and the record still shows what the rules asked for. + + The party must belong to the session in the path (`404` with `"Party does not belong to the given session."` otherwise) and to your application. A PATCH that actually changes the flag fires the `data.updated` [webhook](/integration/webhooks) on the business session with trigger `console_key_people_skip`; a PATCH that sets the flag to its current value is a no-op that echoes the party back. + + Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. + + ``PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`` + + Args: + session_id: UUID of the parent Business Verification (KYB) session. + party_uuid: UUID of the Key People party to update (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`. + is_skipped: `true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateKybPartyResponse", + self._client.request( + "patch", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/", + cast_to=SessionsUpdateKybPartyResponse, + path_params={"sessionId": session_id, "partyUuid": party_uuid}, + auth="api_key", + body={"is_skipped": is_skipped}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_networks( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsListNetworksResponse: + """List networks for a session. + + List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user. + + ``GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/`` + + Args: + session_id: Session UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsListNetworksResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/", + cast_to=SessionsListNetworksResponse, + path_params={"session_id": session_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def preview_resubmission( + self, + session_id: str, + *, + email_address: str, + nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] + | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsPreviewResubmissionResponse: + """Preview a resubmission email. + + Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires write:sessions and an application-scoped session in Approved, Declined, In Review, Kyc Expired or Abandoned status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL. + + The response includes the recipient, sender, subject, HTML, resolved steps and a signed preview_token. requires_user_action describes whether the selected steps include an interactive check. Email delivery remains conditional: if automatic checks finalize the verification during the subsequent resubmission, no action-request email is sent. + + This preview does not reserve session state or authorize delivery. After reviewing it, call Update Session Status with new_status=Resubmitted, the same selection, recipient and language, and email_preview_token set to the returned preview_token. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return 409 before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use Cache-Control: no-store. Render HTML in a sandboxed frame, never directly in your application DOM. + + ``POST /v3/session/{sessionId}/resubmission-preview/`` + + Args: + session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + email_address: Recipient shown in the preview. No email is sent. + nodes_to_resubmit: Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission. + email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsPreviewResubmissionResponse", + self._client.request( + "post", + "/v3/session/{sessionId}/resubmission-preview/", + cast_to=SessionsPreviewResubmissionResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "email_address": email_address, + "nodes_to_resubmit": nodes_to_resubmit, + "email_language": email_language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncSessions(AsyncAPIResource): + """The ``sessions`` resource of the Didit API. + + Methods: ``batch_delete``, ``bulk_update_aml_hit_status``, ``create``, ``delete``, ``generate_pdf``, ``import_shared``, ``list``, ``list_networks``, ``preview_resubmission``, ``retrieve_decision``, ``retrieve_deletion_job``, ``share``, ``update_aml_hit_status``, ``update_data``, ``update_feature_status``, ``update_kyb_company_data``, ``update_kyb_party``, ``update_poa_data``, ``update_status``. + + Nested resources: ``imports``, ``reviews``. + """ + + @cached_property + def imports(self) -> AsyncSessionsImports: + from didit.resources.sessions.imports import AsyncSessionsImports + + return AsyncSessionsImports(self._client) + + @cached_property + def reviews(self) -> AsyncSessionsReviews: + from didit.resources.sessions.reviews import AsyncSessionsReviews + + return AsyncSessionsReviews(self._client) + + @cached_property + def with_raw_response(self) -> AsyncSessionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncSessionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncSessionsWithStreamingResponse(self) + + async def create( + self, + *, + workflow_id: str, + vendor_data: str | NotGiven = NOT_GIVEN, + callback: str | NotGiven = NOT_GIVEN, + callback_method: Literal["initiator", "completer", "both"] | str | NotGiven = NOT_GIVEN, + metadata: object | NotGiven = NOT_GIVEN, + language: Literal[ + "en", + "ar", + "bg", + "bn", + "bs", + "ca", + "cnr", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh", + ] + | str + | NotGiven = NOT_GIVEN, + contact_details: SessionsCreateParamsContactDetails | NotGiven = NOT_GIVEN, + expected_details: SessionsCreateParamsExpectedDetails | NotGiven = NOT_GIVEN, + portrait_image: str | NotGiven = NOT_GIVEN, + sandbox_scenario: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsCreateResponse: + """Create a verification session. + + Create a User Verification (KYC) or Business Verification (KYB) session and receive a hosted verification `url` plus a `session_token` to redirect or embed for your end user. The `workflow_id` selects which verification steps run and whether the session is KYC or KYB. + + Call this from your backend (never the browser - the API key is a secret) whenever a user or business needs to be verified: at signup, before a sensitive action, or when re-verification is required. Prerequisites: a published workflow (created in the Console [Workflows](https://docs.didit.me/console/workflows) page) and an application API key with available credits. Sandbox applications bypass the credit check. + + **Idempotency:** when `vendor_data` is provided and an unfinished session (`Not Started`, `In Progress`, `Resubmitted`, or `Awaiting User`) with the same `vendor_data` already exists on the workflow's **current latest published version** (and that session already has a hosted verification URL), the existing session is returned (still `201`) instead of creating a duplicate - with its `callback` and `metadata` updated to the newly provided values. An unfinished session created on an older version of the workflow (one that has since been republished) is **not** reused; a new session pinned to the latest published version is created instead. Sessions in `Approved`, `Declined`, `In Review`, `Expired`, `Abandoned`, or `Kyc Expired` are never reused. + + **Side effects:** the hosted verification URL is generated and stored; if `contact_details.send_notification_emails` is `true` and an email is provided, a verification invite email is sent. The session expires after the workflow's configured session expiration time. For KYB workflows, `contact_details.phone` and `expected_details` are ignored. + + ``POST /v3/session/`` + + Args: + workflow_id: Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB. + vendor_data: A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection. + callback: URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used. + callback_method: Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`. + metadata: Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context. + language: Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages). + contact_details: User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number. + expected_details: Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored. + portrait_image: Base64-encoded portrait image of the end user's face (max 2MB; JPEG, PNG, WebP, or TIFF). Used as the reference image to match against the liveness capture when the workflow is a `Biometric Authentication` workflow with Face Match enabled, or any graph workflow where Face Match runs before ID Verification (OCR). Optional when the `vendor_data` user already has a stored face image: Didit reuses it automatically, resolved in priority order: approved liveness face, ePassport chip photo, ID document portrait, manually enrolled profile face (only faces from approved sessions or faces you enrolled yourself are reused). When provided, `portrait_image` always takes precedence over the stored face. If omitted and no stored face exists (or `vendor_data` is not sent), session creation fails with `400`. Ignored for other workflow types. + sandbox_scenario: Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsCreateResponse", + await self._client.request( + "post", + "/v3/session/", + cast_to=SessionsCreateResponse, + auth="api_key", + body={ + "workflow_id": workflow_id, + "vendor_data": vendor_data, + "callback": callback, + "callback_method": callback_method, + "metadata": metadata, + "language": language, + "contact_details": contact_details, + "expected_details": expected_details, + "portrait_image": portrait_image, + "sandbox_scenario": sandbox_scenario, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_decision( + self, + session_id: str, + *, + include: Literal["events", "resubmissions", "events,resubmissions"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsRetrieveDecisionResponse: + """Retrieve the full decision for a verification session. + + Retrieve the complete decision report for a verification session - the rolled-up session `status` plus one report per verification feature the workflow executed. This is the canonical read endpoint for both User Verification (KYC) and Business Verification (KYB) sessions: the same path serves both, and the top-level `session_kind` discriminator tells you which shape you received (`"user"` or `"business"`). + + **Every feature report is a plural array.** V3 workflows are graphs that can run the same feature more than once, so each block (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`) is a JSON array whose items each carry a `node_id` identifying the workflow graph node that produced them. Read NFC as `response.nfc_verifications[0]` - never as a singular `nfc` field. A block is `null` until the workflow has run that feature at least once; features the workflow does not include never appear with data. + + **When to call.** The recommended pattern is event-driven: wait for the `status.updated` webhook that fires when the session reaches a decision (`Approved`, `Declined`, `In Review`), then call this endpoint once to fetch the full report. Polling also works - the endpoint can be called at any point in the session lifecycle and returns the data produced so far - but webhook-then-fetch is cheaper and faster. Media URLs in the response (document images, videos, PDFs) are short-lived presigned links: fetch them promptly or re-request the decision to get fresh ones, and do not persist them as long-term references. + + **Statuses.** The top-level `status` is the session lifecycle status (`Not Started`, `In Progress`, `Awaiting User`, `In Review`, `Approved`, `Declined`, `Resubmitted`, `Expired`, `Kyc Expired`, `Abandoned`). Each feature item additionally carries its own feature-level `status` (`Not Finished`, `Approved`, `Declined`, `In Review`, ...). A session can be `Approved` overall while an individual feature is `In Review`, depending on the workflow's decision rules. + + **Authentication and visibility.** Authenticate with the application API key in the `x-api-key` header (a console user Bearer token with the `read:sessions` permission also works). Only sessions owned by the authenticated application are visible. If your workflow restricts response attributes, non-allowed fields inside feature items are returned as `null` while `status`, `warnings`, and `node_id` are always preserved. + + **Rate limits.** GET requests are limited to 600 per minute per API key. Exceeding the limit returns `429` with `Retry-After` and `X-RateLimit-*` headers. + + ``GET /v3/session/{sessionId}/decision/`` + + Args: + session_id: UUID of the verification session whose decision you want to retrieve. This is the `session_id` returned by `POST /v3/session/`. The same path works for both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the id against both session types. **Must be a valid UUID.** The route only matches well-formed UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. Validate the UUID format client-side so you can distinguish an input error from a genuine missing-session `404`. + include: Comma-separated list of extra payload sections. Supported values: `events` and `resubmissions`, alone or together (`events,resubmissions`). When `include=events` is passed, the response additionally contains the session event timeline (`events`, including the `WORKFLOW_BRANCH_EVALUATED` / `WORKFLOW_STATUS_NODE_REACHED` routing events described on the Retrieve Session page), the full activity log (`activity_reviews`), blocklist flags (`blocklisted`), a per-session `cost_breakdown`, workflow metadata (`workflow_type`, `workflow_version`, `workflow_version_id`, and `workflow_terminal_node_id` - the last recorded workflow status node that set a session status, or `null` if none has been recorded; historical provenance that is not cleared by manual approvals, declines or resubmissions, and is replaced by a later workflow status node), and session metadata (`api_service` - string or `null`, set only for standalone-API sessions; `session_type` - `API`, `HOSTED`, or `MIGRATED`; `has_device_nfc_support` - boolean). Two fields change relative to the plain shape: the top-level `session_kind` discriminator is **omitted** (the events shape only exists for user sessions), and the `features` array switches from plain strings to `{feature, node_id}` objects - where the email feature is reported as `EMAIL` instead of `EMAIL_VERIFICATION`. This expanded shape is primarily used by the Didit Console; most API integrations should omit the parameter. **KYB limitation:** `include=events` only applies to user (KYC) sessions. Business sessions (`session_kind = "business"`) always return the standard business decision shape - `events`, `activity_reviews`, `blocklisted`, `cost_breakdown` and the extra `workflow_*`/session metadata are never returned for business sessions, and their `features` array stays a plain string array. With `include=resubmissions` (user and business sessions alike) the response also contains `resubmissions`: one entry per resubmission request, oldest first, each with `number`, `created_at`, `previous_status`, `nodes`, `reasons`, `requested_by` and `results` (what the resubmitted steps returned before the request, under the same keys and in the same shape as the current results). Each `cost_breakdown` item carries `usage_type`, `price`, `details` and `created_at`: a resubmitted step that runs again is billed as a new item. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsRetrieveDecisionResponse", + await self._client.request( + "get", + "/v3/session/{sessionId}/decision/", + cast_to=SessionsRetrieveDecisionResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"include": include}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + session_id: str, + *, + retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, + face_retention_days: int | NotGiven = NOT_GIVEN, + face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] + | str + | NotGiven = NOT_GIVEN, + instruction_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsDeleteResponse: + """Delete a verification session (KYC or KYB). + + Delete a single verification session, User Verification (KYC) or Business Verification (KYB), by its `session_id`. The id is resolved against KYC sessions first, then KYB sessions, so both kinds are deleted through this one URL. + + **What is deleted:** + - The session and every verification attempt it holds, including results that a resubmission replaced: the decision, extracted data, all related feature records (ID verification, face, liveness, face match, AML, registry checks, IP analysis, and so on), its reviews and its activity log. + - All stored files owned by the session: document front/back photos (full, cropped, and privacy-blurred variants), document-capture videos, portrait crops, NFC chip portrait and signature images, face reference images, liveness videos, face-match source/target images, Proof of Address documents, and any extra uploaded files (KYC); uploaded company documents and extra files (KYB). Previously issued media URLs (`https:///...`) stop resolving. + - The session's face data. Its face embedding is deleted too, unless you choose to keep an image-free template (see **Face biometric data**). + - Webhook deliveries still queued for the session, and their retries. Webhooks already delivered are not affected. + - For a Business Verification (KYB) session: the verifications it created for its key people (KYC) and its corporate owners (KYB), recursively, reported as `deleted_child_verifications`. A verification it only links to, such as an existing approved KYC it reused, is not deleted. + + The session disappears from every read endpoint immediately: `GET /v3/session/{sessionId}/decision/` returns `404` and `GET /v3/sessions/` stops listing it. Ongoing AML monitoring and KYB registry monitoring started by the deleted verifications stop. + + **What is not affected:** blocklist entries created from this session (face or document) are **not** removed; manage those with the blocklist endpoints. Share tokens issued for the session are not revoked, but there is nothing left for them to import: an import with one returns `400` with `{"share_token": ["Original session does not exist."]}`. Credits already consumed are **not** refunded. The parent User or Business entity is **not** deleted; use `POST /v3/users/delete/` or `POST /v3/businesses/delete/` for those. If the User's profile portrait came from this session, the portrait is removed with it. + + **Face biometric data:** by default the session's face embedding is deleted with the session (`face_retention_outcome: deleted`). An application that opted in to biometric-template retention (`face_retention_policy: retain_until_user_deleted` on `PATCH /v3/webhook/`), or a request with `retain_face_embeddings: true`, keeps one separately managed, image-free face biometric template anchored to the session's User (`retained_with_user`) so that duplicate detection, Face Search, and biometric authentication keep working. The template never contains the face image, media, identity fields, the decision, or the deleted session's id; it is biometric data deleted at `biometric_template_expires_at`, or earlier on User deletion, privacy erasure, or an explicit purge through `/v3/biometric-templates/`. A session without a linked User cannot retain a template (`ineligible_no_vendor_user`). Send `deletion_instruction: privacy_erasure` for data-subject requests: it purges every retained template for that User and never retains a new one. + + **Irreversible:** deletion is permanent and there is no restore endpoint. Export anything you need (for example a decision PDF) before you call it. + + **Side effects:** no webhook is emitted for deletions. + + **Response:** `200 OK` once the deletion has completed, with a JSON body reporting the `face_retention_outcome`, the retained template's expiry and, for a KYB session, `deleted_child_verifications`. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. Keep your `instruction_id` in your own erasure log. + + **Idempotency:** not idempotent at the HTTP level: the first call returns `200`; repeating it returns `404` because the session no longer resolves. After a `503` the session is still in place; repeat the same request to finish deleting it. + + **Authentication:** send your application's API key in the `x-api-key` header; the session must belong to that application. Console user access tokens (`Authorization: Bearer ...`) may also call this endpoint when they carry the `delete:sessions` permission. Authentication and permission failures both return `403`; this API never returns `401`. + + **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + To bulk-delete KYC sessions by their numeric `session_number`, use `POST /v3/sessions/delete/` instead. + + ``DELETE /v3/session/{sessionId}/delete/`` + + Args: + session_id: UUID (`session_id`) of the User Verification (KYC) or Business Verification (KYB) session to delete, as returned when the session was created. Must be a canonical hyphenated UUID - a non-UUID value does not match the route and returns `404`. + retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. + face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. + face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. + deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). + instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsDeleteResponse", + await self._client.request( + "delete", + "/v3/session/{sessionId}/delete/", + cast_to=SessionsDeleteResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "retain_face_embeddings": retain_face_embeddings, + "face_retention_days": face_retention_days, + "face_retention_deadline": face_retention_deadline, + "deletion_instruction": deletion_instruction, + "instruction_id": instruction_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def generate_pdf( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download a PDF report for a verification session. + + Render and download a compliance-ready PDF report for a finished verification session. The same path serves both User Verification (KYC) and Business Verification (KYB) sessions - the server resolves the `sessionId` against both session types and picks the matching report template. + + **Eligible statuses.** The session must already carry a final or reviewable status: + + - KYC sessions: `Approved`, `Declined`, `In Review`, or `Kyc Expired`. + - KYB sessions: `Approved`, `Declined`, or `In Review`. + + Any other status (`Not Started`, `In Progress`, `Expired`, `Abandoned`, …) returns `403` with an explanatory `detail` string - see the `403` response below. + + **What the report contains.** The PDF mirrors the console session view at the moment of the request: + + - **KYC** - rolled-up session status, the warnings list, and one section per executed feature: ID Verification (document images plus extracted fields), NFC, Liveness, Face Match, Email Verification, Phone Verification, Proof of Address, Questionnaire answers, Database Validation, AML Screening, and IP Analysis - followed by session tags, console review activity (comments and status changes), and the session event timeline. + - **KYB** - registry checks (company data), AML screenings, business document verifications, key-people checks, questionnaire responses, phone/email verifications, IP analyses, and the session event timeline, followed by tags and review activity. + + **Branding.** If your application has white-label customization, the report is rendered with your logo and links to your privacy-policy URL; otherwise it carries Didit branding. Reports are rendered in English - there is no language parameter. + + **No caching.** Every call re-renders the PDF from the current session data, so a report generated after a manual review reflects the reviewer’s decision. Two calls for the same session can produce byte-different files - archive the downloaded file if you need an immutable copy. + + **Latency.** Rendering downloads every stored image (document sides, selfies, extra files) before composing the PDF, so media-heavy sessions can take several seconds. Use a generous client read timeout (60 s recommended) and stream the body to disk. + + **Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response - there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples. + + If you need the underlying data as JSON instead of a rendered report, use `GET /v3/session/{sessionId}/decision/`. For every reportable session of one user in a single document, use `GET /v3/users/{vendor_data}/generate-pdf/`. + + ``GET /v3/session/{sessionId}/generate-pdf/`` + + Args: + session_id: UUID of the User Verification (KYC) or Business Verification (KYB) session to render - the `session_id` returned by `POST /v3/session/`. The same path works for both session kinds; use the `Content-Disposition` filename on the response to tell which report type you received. **Must be a well-formed, lowercase UUID.** The route only matches valid UUIDs, so a malformed value is rejected by the URL router before any application code runs and the response is a `404` **HTML** page rather than the JSON `{"detail": ...}` envelope shown below. The route's UUID converter only matches the canonical lowercase form - uppercase-hex UUIDs are rejected with the HTML 404. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/session/{sessionId}/generate-pdf/", + cast_to=bytes, + path_params={"sessionId": session_id}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_status( + self, + session_id: str, + *, + new_status: Literal["Approved", "Declined", "Resubmitted"] | str, + comment: str | NotGiven = NOT_GIVEN, + nodes_to_resubmit: Sequence[SessionsUpdateStatusParamsNodesToResubmit] + | NotGiven = NOT_GIVEN, + send_email: bool | NotGiven = NOT_GIVEN, + email_address: str | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + email_preview_token: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateStatusResponse: + """Approve, decline, or request resubmission for a verification session. + + Manually override the final decision of a KYC or KYB verification session. Set `new_status` to `Approved` or `Declined` to record a manual decision, or to `Resubmitted` to clear specific verification steps and let the user redo them through the same verification URL. The endpoint accepts both user (KYC) and business (KYB) session IDs. + + Call it once a session has reached a reviewable state: the current status must be `Approved`, `Declined`, `In Review`, `Kyc Expired`, `Abandoned`, or `Resubmitted`. Sessions that are still `Not Started`, `In Progress`, or `Awaiting User`, or that ended as `Expired`, return `400`. The new status must differ from the current one, and your API key's organization needs the `write:sessions` permission. The eligible-state check runs before body validation, so an ineligible session returns the `Wrong Current Status` error even when the payload is also invalid. + + For `Resubmitted`, pass `nodes_to_resubmit` to choose which workflow steps the user must redo, or omit it to auto-select existing feature attempts in a resubmittable state (`Declined`, `In Review`, `Not Finished`, `Expired`). Never-attempted features are not auto-selected - list them explicitly in `nodes_to_resubmit`, or the request returns `400` when no recorded attempt needs resubmission. Captured data for the selected steps is deleted, the steps are reordered to match the workflow graph, the session's expiration window restarts, and the original verification URL becomes usable again. Backend-only steps (`AML`, `DATABASE_VALIDATION`, `IP_ANALYSIS`) at the head of the list run immediately without user interaction; if every resubmitted step is backend-only, the session re-finalizes within the same request. `KYB_REGISTRY` and `KYB_KEY_PEOPLE` cannot be resubmitted directly - resubmit the relevant child KYC sessions instead and the parent business session recomputes automatically. + + Every successful call fires the `status.updated` [webhook](/integration/webhooks) once the change commits and appends an activity entry - with actor attribution and your `comment` - to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and in the console activity timeline. The user-profile aggregates linked to the session's `vendor_data` update as well. Declining disables ongoing AML monitoring for the session; re-approving re-enables it (from `Resubmitted`, `Declined`, or `Kyc Expired`) when the workflow has monitoring turned on. Approving or declining a child KYC that belongs to a KYB key-people check recomputes the parent business session's status. Set `send_email: true` (with `email_address`) to notify the user: a status notice for `Approved`/`Declined`, or a resubmission email containing the verification link and per-step reasons for `Resubmitted`. + + The operation is intentionally not repeatable with the same value: re-sending the current status returns `400` (`The new status is the same as the current status.`), so accidental double-submissions are harmless. Authentication and permission failures both return `403` - this API never responds `401`. + + ``PATCH /v3/session/{sessionId}/update-status/`` + + Args: + session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + new_status: Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`. + comment: Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`. + nodes_to_resubmit: Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) - features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` ("No features found that need resubmission") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` - those parent checks recompute from their child KYC sessions. + send_email: Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons. + email_address: Recipient for the notification email. **Required when `send_email` is `true`** - omitting it returns `400`. + email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). + email_preview_token: Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateStatusResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-status/", + cast_to=SessionsUpdateStatusResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "new_status": new_status, + "comment": comment, + "nodes_to_resubmit": nodes_to_resubmit, + "send_email": send_email, + "email_address": email_address, + "email_language": email_language, + "email_preview_token": email_preview_token, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list( + self, + *, + session_kind: Literal["user", "business", "all"] | str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + status: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + date_from: str | _dt.date | NotGiven = NOT_GIVEN, + date_to: str | _dt.date | NotGiven = NOT_GIVEN, + country: str | NotGiven = NOT_GIVEN, + document_type: str | NotGiven = NOT_GIVEN, + document_name: str | NotGiven = NOT_GIVEN, + region: str | NotGiven = NOT_GIVEN, + didit_internal_id: str | NotGiven = NOT_GIVEN, + session_type: str | NotGiven = NOT_GIVEN, + api_service: Literal[ + "ID_VERIFICATION", + "FACE_MATCH", + "AGE_ESTIMATION", + "FACE_SEARCH", + "POA", + "AML", + "KYB_REGISTRY", + "PASSIVE_LIVENESS", + "DATABASE_VALIDATION", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + ] + | str + | NotGiven = NOT_GIVEN, + screened_full_name: str | NotGiven = NOT_GIVEN, + warning: str | NotGiven = NOT_GIVEN, + status_initialized_as_in_review: bool | NotGiven = NOT_GIVEN, + is_kyc_to_be_reviewed: bool | NotGiven = NOT_GIVEN, + euuid: str | NotGiven = NOT_GIVEN, + organization_name: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[ + SessionsListResponseResultsKycSessionRowSessionKindUser + | SessionsListResponseResultsKybSessionRowSessionKindBusiness + ]: + """List verification sessions (KYC and/or KYB) with filters and pagination. + + Returns a paginated list of verification sessions for the application identified by your API key, newest first (descending `session_number`). + + **Canonical URL:** the route is `/v3/sessions/` (trailing slash). Requests to `/v3/sessions` without the slash receive a `301` redirect to the slashed URL with the query string preserved; most HTTP clients follow it transparently on GET. + + **When to use this endpoint vs webhooks.** Use this endpoint for dashboards, back-office search, reconciliation, and batch exports. Do not poll it to track the progress of individual verifications - configure [webhooks](/integration/webhooks) instead, which push `status.updated` events the moment a session changes. For one session's full result, call `GET /v3/session/{sessionId}/decision/`. + + **KYC and KYB sessions.** `session_kind` selects which kinds are listed: + - `user` (default) - User Verification (KYC) sessions only. Backward-compatible behavior. + - `business` - Business Verification (KYB) sessions only. Rows have a different, smaller shape (see the response schema). + - `all` - both kinds in one response: the KYC page and the KYB page for the same `limit`/`offset` are concatenated in `results` (KYC rows first), so a page can contain up to `2 x limit` rows. `count` is the sum of both kinds and `next`/`previous` reflect the KYC-side pagination. For strict pagination of mixed data, call `user` and `business` separately. + + Every row carries a `session_kind` discriminator (`"user"` or `"business"`). + + **Pagination.** Standard limit/offset: `limit` (default 50) and `offset` (default 0), with `next`/`previous` ready-made page URLs. For KYC rows `count` is exact. For KYB rows `count` is computed with a bounded scan and is capped at 100 - when there are more than 100 matches it stays at 100, so iterate using `next` (or by checking whether a full page came back) rather than computing total pages from `count`. + + **Filter semantics.** All filters combine with AND and apply to KYC rows. When `session_kind` is `business` or `all`, the KYB side honors the shared parameters `vendor_data`, `status`, `workflow_id`, `country` (registry-company country code), `date_from`, `date_to`, `search` (company name, registration number - type registration numbers without separators, e.g. `DPC001` not `DPC-001` - vendor_data, session number, or session UUID), `warning`, and `organization_name`; the remaining KYC-only filters are ignored for KYB rows. Comma-separated parameters (`status`, `country`, `document_type`, `region`, `workflow_id`, `document_name`, `warning`, `organization_name`, `session_type`) match any of the listed values (OR within the parameter). `vendor_data` is an exact match - use `search` for partial matching. Unknown values in list filters simply return no rows (no error); malformed typed values (`date_from`, `date_to`, `didit_internal_id`, `api_service`, `session_kind`) return `400` with a per-field error object. + + **Search.** `search` adapts to the input: a full UUID looks up `session_id`; digits (with optional `+ - . space`) match session-number prefix, document/personal numbers, phone-number prefix, or `vendor_data`; values containing `@` match email prefixes; any other text matches names (accent-insensitive) and `vendor_data`. + + No fixed per-endpoint rate limit is enforced, but keep list polling infrequent and rely on webhooks for real-time updates. + + ``GET /v3/sessions/`` + + Args: + session_kind: Which kind of sessions to list: `user` (KYC only, default), `business` (KYB only), or `all` (both, concatenated - see the description for merged-pagination semantics). Matching is case-insensitive (`BUSINESS` works); any value other than a case variant of `user`/`business`/`all` returns `400`. + limit: Maximum number of rows per page (per kind when `session_kind=all`). Defaults to 50. + offset: Number of rows to skip before the first returned row. Defaults to 0. Prefer following the `next`/`previous` URLs from the response. + status: Comma-separated list of session statuses to include, e.g. `Approved,In Review`. Values are case-sensitive; see [Verification Statuses](/integration/verification-statuses). Unknown values match nothing. + vendor_data: Exact-match filter on the `vendor_data` you sent at session creation (your own user/business identifier). For partial matches use `search` instead. + search: Smart free-text search. A full UUID matches `session_id`; numeric input matches session-number prefix, document/personal numbers, phone prefix, or `vendor_data`; input containing `@` matches email prefixes; other text matches names (accent-insensitive) and `vendor_data`. Free text containing digits additionally matches document/personal-number search fields and session-number prefixes. + workflow_id: Comma-separated workflow UUIDs (find them on the Workflows page in the Console). Malformed UUIDs in the list are ignored; if none are valid the result is empty. + date_from: Only sessions created on or after this date (UTC), format `YYYY-MM-DD`. Malformed dates return `400`. + date_to: Only sessions created up to this date (UTC), format `YYYY-MM-DD`. For KYC rows the entire day is included; for KYB rows the comparison is `created_at <= date_to` (midnight), so use the next day's date to include a full day of KYB sessions. + country: Comma-separated country filter. For KYC rows: ISO 3166-1 alpha-3 codes of the ID document's issuing state (e.g. `ESP,USA`), which may differ from nationality. For KYB rows: the registry company's country code. + document_type: Comma-separated ID-document type codes verified in the session (KYC rows only): `P` (Passport), `ID` (Identity Card), `DL` (Driver's License), `RP` (Residence Permit), `SSC` (Social Security Card), `HIC` (Health Insurance Card), `WP` (Work Permit), `TC` (Tax Card), `VISA`, `PSC` (Public Service Card), `BC` (Birth Certificate), `OTHER`. Note the response field `document_type` contains the display name, not the code. + document_name: Exact document names; multiple names can be passed comma-separated, but the value is only split on commas when it contains more than one ` - ` separator (a lone name with an embedded comma is matched whole). Names are as recognized by document verification (front or back side), e.g. `Spain - Id Card (2021)`. KYC rows only. + region: Comma-separated document region/state values (for documents issued per region, such as US driver's licenses). KYC rows only. + didit_internal_id: Filter by Didit's internal user identifier (the `didit_internal_id` response field) to list all sessions of one verified user. Must be a valid UUID; malformed values return `400`. KYC rows only. + session_type: Comma-separated session creation channels: `API`, `HOSTED`, `MIGRATED`. The special value `ALL` (or omitting the parameter) disables the filter. KYC rows only. + api_service: Filter standalone API sessions by service. Invalid values return `400`. KYC rows only. + screened_full_name: Accent-insensitive name search inside AML and database-validation screened data. When combined with `api_service=AML` or `api_service=DATABASE_VALIDATION`, the search is restricted to that feature's screened data. KYC rows only. + warning: Comma-separated warning risk codes; returns sessions that raised any of them (e.g. `POSSIBLE_FRAUD`). Risk codes are listed per feature in the [warnings documentation](/integration/webhooks). Applies to KYC rows and, with business warning codes, to KYB rows. + status_initialized_as_in_review: When `true`, only sessions whose decision initially landed in `In Review` (useful to audit manual-review volume). KYC rows only. + is_kyc_to_be_reviewed: `true` returns sessions whose document verification is flagged for manual review; `false` returns sessions with document verification explicitly not flagged. Omit to disable. KYC rows only. + euuid: Filter by the end user's euuid (UUID assigned to the verifying user, used by Console deep links). Must be a valid UUID; malformed values return a 400 field error. KYC rows only. + organization_name: Comma-separated organization names. Console-oriented; with an API key (already scoped to one application) it only matches your own organization's name. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[SessionsListResponseResultsKycSessionRowSessionKindUser | SessionsListResponseResultsKybSessionRowSessionKindBusiness]", + self._client.request_page( + "get", + "/v3/sessions/", + cast_to=SessionsListResponse, + item_type=SessionsListResponseResultsKycSessionRowSessionKindUser + | SessionsListResponseResultsKybSessionRowSessionKindBusiness, + auth="api_key", + query={ + "session_kind": session_kind, + "limit": limit, + "offset": offset, + "status": status, + "vendor_data": vendor_data, + "search": search, + "workflow_id": workflow_id, + "date_from": date_from, + "date_to": date_to, + "country": country, + "document_type": document_type, + "document_name": document_name, + "region": region, + "didit_internal_id": didit_internal_id, + "session_type": session_type, + "api_service": api_service, + "screened_full_name": screened_full_name, + "warning": warning, + "status_initialized_as_in_review": status_initialized_as_in_review, + "is_kyc_to_be_reviewed": is_kyc_to_be_reviewed, + "euuid": euuid, + "organization_name": organization_name, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def share( + self, + session_id: str, + *, + for_application_id: str, + ttl_in_seconds: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsShareResponse: + """Mint a share token for a finished verification session. + + Mint a short-lived JWT that lets a specific Didit application import this finished session. Pair with `POST /v3/session/import-shared/`. + + ``POST /v3/session/{sessionId}/share/`` + + Args: + session_id: UUID of the source verification session to mint a token for. + for_application_id: UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**. + ttl_in_seconds: Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsShareResponse", + await self._client.request( + "post", + "/v3/session/{sessionId}/share/", + cast_to=SessionsShareResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={"for_application_id": for_application_id, "ttl_in_seconds": ttl_in_seconds}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def import_shared( + self, + *, + share_token: str, + trust_review: bool, + workflow_id: str, + vendor_data: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Redeem a share token to clone a verification session into your application. + + Redeem a share token to clone a KYC or KYB session into the calling application with a fresh `session_id`. Not idempotent - a token redeems once per receiver. + + ``POST /v3/session/import-shared/`` + + Args: + share_token: JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session). + trust_review: If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`. + workflow_id: UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`. + vendor_data: Optional override for the cloned session's `vendor_data`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + await self._client.request( + "post", + "/v3/session/import-shared/", + cast_to=object, + auth="api_key", + body={ + "share_token": share_token, + "trust_review": trust_review, + "workflow_id": workflow_id, + "vendor_data": vendor_data, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def batch_delete( + self, + *, + session_numbers: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + retain_face_embeddings: bool | None | NotGiven = NOT_GIVEN, + face_retention_days: int | NotGiven = NOT_GIVEN, + face_retention_deadline: str | _dt.datetime | NotGiven = NOT_GIVEN, + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] + | str + | NotGiven = NOT_GIVEN, + instruction_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202: + """Bulk delete KYC sessions by session_number. + + Bulk delete User Verification (KYC) sessions by their numeric `session_number`, or every KYC session in your application with `delete_all: true`. + + `session_number` is the human-friendly incrementing counter shown in the Console session list and returned as `session_number` by the create-session and decision endpoints; it is **not** the `session_id` UUID. To delete a session by UUID, or to delete a Business Verification (KYB) session, which this endpoint never touches, use `DELETE /v3/session/{sessionId}/delete/` instead. + + **Up to 20 sessions are deleted in the request, more run as a job.** The number of matching sessions decides, whether they are selected by `session_numbers` or with `delete_all`: + - Up to 20 matching sessions: they are deleted before the response, which is `200` with one entry per session in `results[]`. + - More than 20: the response is `202` with a deletion job, and the sessions are deleted in the background. Poll `GET /v3/sessions/delete/{job_id}/` until `status` is `COMPLETED` or `FAILED`. A job reports counts per outcome, never which sessions it deleted. A `delete_all` job deletes only the sessions created before the request; sessions started while it runs are not deleted. + + Each deleted session gets exactly the same treatment as the single-session delete endpoint: the session, every verification attempt it holds, its decision and extracted data, its related feature records (ID verification, face, liveness, face match, and so on) and all of its stored files are deleted, it disappears from every read endpoint, and previously issued media URLs stop resolving. Webhook deliveries still queued for a deleted session, and their retries, are dropped; webhooks already delivered are not affected. Blocklist entries are **not** removed, credits already consumed are **not** refunded, and the parent User entities are **not** deleted (use `POST /v3/users/delete/` for those); no webhook is emitted for the deletion. Deletion is permanent and irreversible. + + **Selection semantics:** + - Entries in `session_numbers` that do not match a live KYC session in your application (unknown numbers, already-deleted sessions, or sessions owned by another application) are **silently skipped**: they do not appear in `results` and are not counted in a job's `total`. + - Validation failures (a non-numeric entry, an empty list, neither `session_numbers` nor `delete_all` provided, or a template retention the application cannot honour) reject the **whole request** with `400` and delete nothing. + - Sessions are deleted in groups. If a group cannot be deleted (for example because a storage service is unavailable), every session in that group is reported as `face_retention_outcome: failed_retryable` and stays in place; the other groups are unaffected. Repeat the request for those sessions. A job retries such a group by itself. + - There is no documented cap on the list length; the shared write rate limit (below) is the practical bound. + + **Face biometric data:** the optional `retain_face_embeddings`, `face_retention_days`, `face_retention_deadline`, `deletion_instruction`, and `instruction_id` fields have the same meaning as on `DELETE /v3/session/{sessionId}/delete/` and apply to every matched session. Each result reports its own `face_retention_outcome`. + + **Response:** `200 OK` with `results[]` for up to 20 matching sessions, or `202 Accepted` with a deletion job for more. Every result confirms deletion except `face_retention_outcome: failed_retryable`, which reports a session that was NOT deleted and should be retried. Earlier versions of this endpoint returned `204 No Content`; update clients that assert on `204`. + + **Idempotency:** repeating the same request deletes nothing more: numbers that no longer match a session are skipped. + + **`delete_all: true` (destructive):** deletes **every** remaining KYC session in the application; `session_numbers` is ignored when set. There is no confirmation step and no undo. + + **Authentication:** API key in the `x-api-key` header. Missing or invalid credentials return `403` (`{"detail": "You do not have permission to perform this action."}`); this API never returns `401`. + + **Rate limit:** shared write budget of 300 requests/min per API key across all POST/PATCH/DELETE endpoints; exceeding it returns `429`. + + This endpoint accepts API keys only. Business Console user Bearer tokens are rejected with `403` (unlike the single-session delete, which accepts both). + + ``POST /v3/sessions/delete/`` + + Args: + session_numbers: Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped. + delete_all: When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step. + retain_face_embeddings: Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding. + face_retention_days: Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`. + face_retention_deadline: Optional hard expiry for a retained template. Must be in the future. + deletion_instruction: The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`). + instruction_id: Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsBatchDeleteResponse | SessionsBatchDeleteResponse202", + await self._client.request( + "post", + "/v3/sessions/delete/", + cast_to={200: SessionsBatchDeleteResponse, 202: SessionsBatchDeleteResponse202}, + auth="api_key", + body={ + "session_numbers": session_numbers, + "delete_all": delete_all, + "retain_face_embeddings": retain_face_embeddings, + "face_retention_days": face_retention_days, + "face_retention_deadline": face_retention_deadline, + "deletion_instruction": deletion_instruction, + "instruction_id": instruction_id, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_deletion_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsRetrieveDeletionJobResponse: + """Get a bulk session deletion job. + + Progress of a bulk session deletion that `POST /v3/sessions/delete/` answered with `202` (more than 20 matching sessions, selected by `session_numbers` or with `delete_all`). Poll every few seconds while `status` is `PENDING` or `PROCESSING`; stop when it is `COMPLETED` or `FAILED`. + + The job reports how many sessions ended in each outcome and how many it has processed, never which sessions it deleted. A `FAILED` job carries the reason in `error`; the sessions it had not reached are still in place, so repeat the deletion request to delete them. + + **Authentication:** API key in the `x-api-key` header. A job is only visible to the application that created it; any other `job_id` returns `404`. Missing or invalid credentials return `403`; this API never returns `401`. + + ``GET /v3/sessions/delete/{job_id}/`` + + Args: + job_id: The `job_id` returned with the `202` from `POST /v3/sessions/delete/`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsRetrieveDeletionJobResponse", + await self._client.request( + "get", + "/v3/sessions/delete/{job_id}/", + cast_to=SessionsRetrieveDeletionJobResponse, + path_params={"job_id": job_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_data( + self, + session_id: str, + *, + document_type: Literal[ + "P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER" + ] + | str + | None + | NotGiven = NOT_GIVEN, + document_subtype: str | None | NotGiven = NOT_GIVEN, + document_number: str | None | NotGiven = NOT_GIVEN, + personal_number: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + date_of_issue: str | _dt.date | None | NotGiven = NOT_GIVEN, + expiration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + issuing_state: str | None | NotGiven = NOT_GIVEN, + first_name: str | None | NotGiven = NOT_GIVEN, + last_name: str | None | NotGiven = NOT_GIVEN, + gender: Literal["M", "F", "U"] | str | None | NotGiven = NOT_GIVEN, + address: str | None | NotGiven = NOT_GIVEN, + place_of_birth: str | None | NotGiven = NOT_GIVEN, + nationality: str | None | NotGiven = NOT_GIVEN, + marital_status: Literal["SINGLE", "MARRIED", "DIVORCED", "WIDOWED", "UNKNOWN"] + | str + | NotGiven = NOT_GIVEN, + extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + parsed_address: SessionsUpdateDataParamsParsedAddress | None | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateDataResponse: + """Update KYC data extracted from the ID document on a verification session. + + Correct the identity fields that were extracted (OCR / NFC) from the user's ID document - misread names, dates, document numbers, and so on - after the verification has finished. Typical callers are a compliance analyst working from your own back office, or an automated reconciliation job. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). + + The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry an ID-verification (KYC) record: sessions whose workflow never ran an ID-verification step return `404` (`"The session does not have a KYC."`). For graph workflows with multiple ID-verification steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent KYC record is patched. + + Two derived fields are recomputed on every call: `full_name` (from `first_name` + `last_name`) and `issuing_state_name` (from `issuing_state`). If the free-text `address` changes, the server may also re-geocode it and refresh `parsed_address` (skipped for sandbox sessions). `extra_fields` and `parsed_address` are merged key-by-key rather than replaced - see the per-field descriptions for the `null`-to-delete semantics. + + Submitting at least one valid field fires the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) - even when the submitted values equal the stored ones. When values actually change, a `KYC_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. An empty JSON body (`{}`) is accepted and simply returns the current KYC data without firing the webhook. + + Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + + ``PATCH /v3/session/{sessionId}/update-data/`` + + Args: + session_id: UUID of the User Verification (KYC) session whose extracted document data you are correcting. Business (KYB) session IDs are not valid here - for company data use [Update KYB Company Data](#patch-/v3/session/-sessionId-/kyb/-companyUuid-/update-data/). + document_type: Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`. + document_subtype: Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants. + document_number: Document number as printed on the ID. + personal_number: Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport). + date_of_birth: Date of birth in `YYYY-MM-DD`. Any other format returns `400`. + date_of_issue: Issue date of the document in `YYYY-MM-DD`. + expiration_date: Expiration date of the document in `YYYY-MM-DD`. + issuing_state: Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`. + first_name: Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint. + last_name: Family name(s). Also feeds the recomputed `full_name`. + gender: Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value - including `X` - returns `400`. + address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding - the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record. + place_of_birth: Place of birth as printed on the document. + nationality: Nationality as printed on the document. Free text up to 32 characters - typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one. + marital_status: Marital status where the document carries it. Any other value returns `400`. + extra_fields: Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `""` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied). + parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response - you cannot set it directly. + node_id: Workflow graph node whose KYC record to patch, for graph workflows with multiple ID-verification steps (also accepted as a `node_id` body field). When omitted, the most recent KYC record on the session is patched. If no KYC exists for the given node, the call returns `404` (`"The session does not have a KYC for node ''."`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateDataResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-data/", + cast_to=SessionsUpdateDataResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id}, + body={ + "document_type": document_type, + "document_subtype": document_subtype, + "document_number": document_number, + "personal_number": personal_number, + "date_of_birth": date_of_birth, + "date_of_issue": date_of_issue, + "expiration_date": expiration_date, + "issuing_state": issuing_state, + "first_name": first_name, + "last_name": last_name, + "gender": gender, + "address": address, + "place_of_birth": place_of_birth, + "nationality": nationality, + "marital_status": marital_status, + "extra_fields": extra_fields, + "parsed_address": parsed_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_feature_status( + self, + session_id: str, + node_id: str, + *, + new_status: Literal["Approved", "Declined", "In Review"] | str, + comment: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateFeatureStatusResponse: + """Update an individual feature (step) status. + + Set the status of a single verification feature (step) within a session **without changing the overall session decision**. Target the feature by its workflow graph `nodeId` (use `default` for non-graph workflows where the step has no node). + + Works for both User Verification (KYC) and Business Verification (KYB) sessions and every step type - ID/OCR, Liveness, Face Match, NFC, AML, Proof of Address, Phone, Email, Database Validation, Questionnaire, and the KYB registry / documents / key-people checks. + + `new_status` must be one of `Approved`, `Declined`, or `In Review`, and must differ from the feature's current status. A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. This endpoint is **not** available for standalone API sessions (`session_type = API`). + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + node_id: Workflow graph node ID of the feature to update (for example `feature_ocr_1`). Use the literal value `default` for non-graph (single-step) workflows where the feature has no node ID. + new_status: Target status for this feature. Must differ from the current value. + comment: Optional free-text note recorded on the session's review trail for this feature change. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateFeatureStatusResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/features/{nodeId}/update-status/", + cast_to=SessionsUpdateFeatureStatusResponse, + path_params={"sessionId": session_id, "nodeId": node_id}, + auth="api_key", + body={"new_status": new_status, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_aml_hit_status( + self, + session_id: str, + *, + hit_id: str, + review_status: Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] + | str, + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Update an AML hit review status. + + Set the review status of a **single AML hit** (a potential sanction / PEP / watchlist / adverse-media match) returned by AML screening, without changing the overall session decision. Identify the hit by its `hit_id` (the `id` of an entry in the AML check's `hits` array - see the session decision). + + `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. Reviewer decisions are preserved across ongoing-monitoring updates, so a hit you mark `False Positive` stays that way when the provider re-screens. + + A successful call fires a `data.updated` webhook and appends an activity entry (with actor attribution) to the session's review trail. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/update-aml-hit-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + hit_id: The `id` of the hit in the AML check's `hits` array. + review_status: The new review status for the hit. + node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. + node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-aml-hit-status/", + cast_to=object, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id_query}, + body={"hit_id": hit_id, "review_status": review_status, "node_id": node_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def bulk_update_aml_hit_status( + self, + session_id: str, + *, + hit_updates: Sequence[SessionsBulkUpdateAmlHitStatusParamsHitUpdates], + node_id: str | NotGiven = NOT_GIVEN, + node_id_query: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Bulk update AML hit review statuses. + + Update the review status of **multiple AML hits** in one call. Provide `hit_updates`, a list of `{ hit_id, review_status }` pairs. `review_status` is one of `Unreviewed`, `Confirmed Match`, `False Positive`, or `Inconclusive`. + + The update is all-or-nothing: if any `hit_id` is not found in the AML check, the request returns `404` and no hit is changed. A successful call fires a single `data.updated` webhook and records one activity entry covering all changes. For graph workflows with multiple AML checks, pass `node_id` to target a specific check. + + Authenticate with your API key (`x-api-key`) or a user authorization header - either must carry the `write:sessions` privilege. + + ``PATCH /v3/session/{sessionId}/bulk-update-aml-hit-status/`` + + Args: + session_id: UUID of the verification session. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + hit_updates: Non-empty list of hit updates. + node_id: Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks. + node_id_query: Workflow graph node ID. Required only when the session has multiple AML checks (one per node); omit it for single-AML sessions. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + await self._client.request( + "patch", + "/v3/session/{sessionId}/bulk-update-aml-hit-status/", + cast_to=object, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id_query}, + body={"hit_updates": hit_updates, "node_id": node_id}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_poa_data( + self, + session_id: str, + *, + issuing_state: str | None | NotGiven = NOT_GIVEN, + document_type: Literal[ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN", + ] + | str + | NotGiven = NOT_GIVEN, + document_language: str | None | NotGiven = NOT_GIVEN, + issuer: str | None | NotGiven = NOT_GIVEN, + issue_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + poa_address: str | None | NotGiven = NOT_GIVEN, + name_on_document: str | None | NotGiven = NOT_GIVEN, + extra_fields: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + poa_parsed_address: SessionsUpdatePoaDataParamsPoaParsedAddress + | None + | NotGiven = NOT_GIVEN, + node_id: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdatePoaDataResponse: + """Update Proof of Address data extracted from the POA document on a verification session. + + Correct the fields extracted from the user's Proof of Address document - utility bill, bank statement, or other address evidence - after the verification has finished: issuer, dates, the printed name, the address itself, and banking details. Every field in the body is optional: only the keys you send are written, everything else is left untouched (`PATCH` semantics). + + The session must belong to your application and be in `Approved`, `Declined`, `In Review`, or `Kyc Expired` status. Sessions in any other state (e.g. still `In Progress`) return `404` with `"No Session matches the given query."` - distinct from the `"Not found."` returned for an unknown or deleted `sessionId`. The session must also carry a POA record: sessions whose workflow never ran a Proof of Address step return `404` (`"The session does not have a POA."`). For graph workflows with multiple POA steps, target a specific step with `node_id` (query parameter or body field); when omitted, the most recent POA record is patched. + + `extra_fields` is merged key-by-key: nine known keys (`bank_iban`, `additional_names`, …) write through to dedicated POA columns, any other key is kept as a custom extra, and `null` / `""` deletes a key. `poa_parsed_address` accepts only the seven documented keys and recomputes `formatted_address` / `poa_formatted_address` after every edit. If the free-text `poa_address` changes, the server may also re-geocode it and refresh the structured address (skipped for sandbox sessions). + + Unlike the KYC variant of this endpoint, the `data.updated` [webhook](/integration/webhooks) (trigger `manual_review`) fires only when at least one stored value **actually changed** - re-sending identical values is a silent no-op. When values change, a `POA_DATA_UPDATED` activity entry recording the changed fields with previous and new values is appended to the session's `reviews`, visible in [Get Decision](#get-/v3/session/-sessionId-/decision/) and the console activity timeline. The session's status and decision are **not** re-evaluated - use [Update Status](#patch-/v3/session/-sessionId-/update-status/) to change the outcome. + + Accepts an API key or a console user token; either way the credential needs the `write:sessions` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. This endpoint has a dedicated rate limit of 10 requests per minute per API key (invalid requests count too); the shared write budget of 300 requests per minute also applies. + + An empty JSON body `{}` is a no-op: it returns `200` echoing the current POA record without firing the webhook (`has_changes` stays false). + + ``PATCH /v3/session/{sessionId}/update-poa-data/`` + + Args: + session_id: UUID of the User Verification (KYC) session whose attached POA record you are correcting. + issuing_state: Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`. + document_type: POA document category. Values are uppercase - lowercase variants like `utility_bill` return `400`. + document_language: Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text - not validated as a language code. + issuer: Organization that issued the document (utility company, bank, government agency). + issue_date: Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`. + poa_address: Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record. + name_on_document: Recipient name as printed on the document. + extra_fields: Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `""` deletes it. Must be a JSON object, otherwise `400`. + poa_parsed_address: Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` - you cannot set it directly. + node_id: Workflow graph node whose POA record to patch, for graph workflows with multiple Proof of Address steps (also accepted as a `node_id` body field). When omitted, the most recent POA record on the session is patched. If no POA exists for the given node, the call returns `404` (`"The session does not have a POA for node ''."`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdatePoaDataResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/update-poa-data/", + cast_to=SessionsUpdatePoaDataResponse, + path_params={"sessionId": session_id}, + auth="api_key", + query={"node_id": node_id}, + body={ + "issuing_state": issuing_state, + "document_type": document_type, + "document_language": document_language, + "issuer": issuer, + "issue_date": issue_date, + "poa_address": poa_address, + "name_on_document": name_on_document, + "extra_fields": extra_fields, + "poa_parsed_address": poa_parsed_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_kyb_company_data( + self, + session_id: str, + company_uuid: str, + *, + company_name: str | NotGiven = NOT_GIVEN, + registration_number: str | NotGiven = NOT_GIVEN, + country_code: str | NotGiven = NOT_GIVEN, + region: str | None | NotGiven = NOT_GIVEN, + incorporation_date: str | _dt.date | NotGiven = NOT_GIVEN, + registration_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + dissolution_date: str | _dt.date | None | NotGiven = NOT_GIVEN, + tax_number: str | NotGiven = NOT_GIVEN, + legal_address: str | NotGiven = NOT_GIVEN, + company_type: str | NotGiven = NOT_GIVEN, + registry_status: Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + | NotGiven = NOT_GIVEN, + verification_status: Literal["verified", "failed", "unknown"] + | str + | None + | NotGiven = NOT_GIVEN, + alternative_names: str | NotGiven = NOT_GIVEN, + nature_of_business: str | NotGiven = NOT_GIVEN, + registered_capital: str | NotGiven = NOT_GIVEN, + registered_capital_amount: float | None | NotGiven = NOT_GIVEN, + registered_capital_currency: str | None | NotGiven = NOT_GIVEN, + website: str | NotGiven = NOT_GIVEN, + email: str | NotGiven = NOT_GIVEN, + phone: str | NotGiven = NOT_GIVEN, + legal_entity_identifier: str | NotGiven = NOT_GIVEN, + location_of_registration: str | NotGiven = NOT_GIVEN, + vat_number: str | NotGiven = NOT_GIVEN, + control_scheme: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateKybCompanyDataResponse: + """Update registry-extracted company data on a Business Verification (KYB) session. + + Correct the registry-extracted company data on a Business Verification (KYB) session - company name, registration number, addresses, status, contact details - identified by `companyUuid`. Typical callers are a compliance analyst fixing a registry misread, or your back office syncing corrected company data. Every field in the body is optional (`PATCH` semantics); an empty JSON body (`{}`) is a no-op that simply echoes the current company record. + + Edits are written in two places at once: fields with a canonical company column (name, registration number, country, incorporation date, tax number, type, statuses, capital fields (`registered_capital`, `registered_capital_amount`, `registered_capital_currency`), alternative names, nature of business, website, email, phone, LEI, location, and `legal_address` → `registered_address`) update that column, and **every** submitted key is also merged into the `registry_data` snapshot (dates as ISO strings) so the "extracted from registry" view reflects the correction. Fields without a column - `region`, `registration_date`, `dissolution_date`, `control_scheme` - land in `registry_data` only. `user_provided_data` (what the end user confirmed in the hosted flow) is never touched, and `last_console_edit_at` is stamped on every non-empty PATCH. + + Unlike the KYC/POA update endpoints, there is **no session-status gate**: the company can be edited at any point in the business session's lifecycle. The company must belong to the session in the path (`404` with `"KYB company does not belong to the given session."` otherwise) and to your application. Every non-empty PATCH fires the `data.updated` [webhook](/integration/webhooks) on the business session (trigger `console_registry_edit`) - even when the submitted values equal the stored ones. The session's status and decision are **not** re-evaluated, and no review/audit entry is recorded. + + Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. Authentication and permission failures both return `403` - this API never responds `401`. Note that the session lookup runs before credential checks, so an unknown `sessionId` returns `404` even with missing credentials. The shared write budget of 300 requests per minute per API key applies. + + For User Verification sessions, the KYC counterpart is `PATCH /v3/session/{sessionId}/update-data/`; to change the session decision itself use `PATCH /v3/session/{sessionId}/update-status/`. + + ``PATCH /v3/session/{sessionId}/kyb/{companyUuid}/update-data/`` + + Args: + session_id: UUID of the parent Business Verification (KYB) session. + company_uuid: UUID of the company record to update (`registry_checks[].company.uuid` on the business session decision). Must belong to the session referenced by `sessionId`. + company_name: Legal name of the company. Written to the canonical column and the registry snapshot. + registration_number: Official registration number with the company registry. + country_code: Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`. + region: Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only - it has no canonical column. + incorporation_date: Incorporation date in `YYYY-MM-DD`. Optional like every other field - the `PATCH` is partial. Any other date format returns `400`. + registration_date: Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only. + dissolution_date: Dissolution date for companies that are no longer active. Stored in the registry snapshot only. + tax_number: Tax identification number. + legal_address: Legal / registered address. Aliased server-side to the canonical `registered_address` column - the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent. + company_type: Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`). + registry_status: Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`. + verification_status: Outcome of the registry verification. Any other value returns `400`. + alternative_names: Trade names, former legal names, DBA names (free text). + nature_of_business: Short description of the company's activity. + registered_capital: Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text. + registered_capital_amount: Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `"1500000.50"`). + registered_capital_currency: ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`. + website: Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`. + email: Contact email on file with the registry. + phone: Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total - otherwise `400`. + legal_entity_identifier: LEI, where the company has one. + location_of_registration: City / subdivision of the registry of record. + vat_number: EU VAT number. Merged into the `registry_data` snapshot only - editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged. + control_scheme: Control / governance scheme when the registry exposes it. Stored in the registry snapshot only. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateKybCompanyDataResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/kyb/{companyUuid}/update-data/", + cast_to=SessionsUpdateKybCompanyDataResponse, + path_params={"sessionId": session_id, "companyUuid": company_uuid}, + auth="api_key", + body={ + "company_name": company_name, + "registration_number": registration_number, + "country_code": country_code, + "region": region, + "incorporation_date": incorporation_date, + "registration_date": registration_date, + "dissolution_date": dissolution_date, + "tax_number": tax_number, + "legal_address": legal_address, + "company_type": company_type, + "registry_status": registry_status, + "verification_status": verification_status, + "alternative_names": alternative_names, + "nature_of_business": nature_of_business, + "registered_capital": registered_capital, + "registered_capital_amount": registered_capital_amount, + "registered_capital_currency": registered_capital_currency, + "website": website, + "email": email, + "phone": phone, + "legal_entity_identifier": legal_entity_identifier, + "location_of_registration": location_of_registration, + "vat_number": vat_number, + "control_scheme": control_scheme, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_kyb_party( + self, + session_id: str, + party_uuid: str, + *, + is_skipped: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsUpdateKybPartyResponse: + """Skip (or re-require) a Key People party on a Business Verification (KYB) session. + + Exempt a Key People party from verification, or put it back in scope. This is the operator escape hatch for a party that can never finish its own check - most often because the individual KYC session handed to a UBO or director was deleted, which freezes that child's status and leaves the KYB parked in `Awaiting User` with nothing left that could move it. + + Setting `is_skipped: true` drops the party out of the Key People aggregation and immediately recomputes the `KYBKeyPeopleCheck` and the parent session status, so the business session can settle and then be approved or declined. Sending `is_skipped: false` puts the party back in scope and the check goes back to waiting on it. + + Unlike the applicant-facing skip in the hosted Key People screen, this endpoint is **not** gated by the role's `allow_skip` setting - that flag limits what the applicant may decide, not what your analyst may override. `requires_verification` is deliberately left untouched, so an overridden party reads as `requires_verification: true` **and** `is_skipped: true` and the record still shows what the rules asked for. + + The party must belong to the session in the path (`404` with `"Party does not belong to the given session."` otherwise) and to your application. A PATCH that actually changes the flag fires the `data.updated` [webhook](/integration/webhooks) on the business session with trigger `console_key_people_skip`; a PATCH that sets the flag to its current value is a no-op that echoes the party back. + + Accepts an API key or a console user token; either way the credential needs the `write:businesses` permission. + + ``PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/`` + + Args: + session_id: UUID of the parent Business Verification (KYB) session. + party_uuid: UUID of the Key People party to update (`key_people_checks[].submitted.parties[].uuid` on the business session decision). Must belong to the session referenced by `sessionId`. + is_skipped: `true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsUpdateKybPartyResponse", + await self._client.request( + "patch", + "/v3/session/{sessionId}/kyb/parties/{partyUuid}/", + cast_to=SessionsUpdateKybPartyResponse, + path_params={"sessionId": session_id, "partyUuid": party_uuid}, + auth="api_key", + body={"is_skipped": is_skipped}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_networks( + self, + session_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsListNetworksResponse: + """List networks for a session. + + List the networks the session's vendor user belongs to. Resolves through the session's vendor_data-linked user. + + ``GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/`` + + Args: + session_id: Session UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsListNetworksResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/", + cast_to=SessionsListNetworksResponse, + path_params={"session_id": session_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def preview_resubmission( + self, + session_id: str, + *, + email_address: str, + nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] + | NotGiven = NOT_GIVEN, + email_language: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsPreviewResubmissionResponse: + """Preview a resubmission email. + + Render the email that would accompany a KYC or KYB resubmission without changing the session, resetting checks, extending its expiry, running checks or sending mail. Requires write:sessions and an application-scoped session in Approved, Declined, In Review, Kyc Expired or Abandoned status. The server validates the selected workflow steps and renders its fixed translated template with the applicable verified white-label sender and branding. The request cannot set a subject, message body or action URL. + + The response includes the recipient, sender, subject, HTML, resolved steps and a signed preview_token. requires_user_action describes whether the selected steps include an interactive check. Email delivery remains conditional: if automatic checks finalize the verification during the subsequent resubmission, no action-request email is sent. + + This preview does not reserve session state or authorize delivery. After reviewing it, call Update Session Status with new_status=Resubmitted, the same selection, recipient and language, and email_preview_token set to the returned preview_token. The receipt expires after 15 minutes. When a receipt is supplied, changed inputs, message content, sender or session state return 409 before any mutation or send. Existing integrations may omit the receipt. Regenerate the preview after a conflict. Treat the response as sensitive verification content; responses use Cache-Control: no-store. Render HTML in a sandboxed frame, never directly in your application DOM. + + ``POST /v3/session/{sessionId}/resubmission-preview/`` + + Args: + session_id: UUID of the verification session to update. Accepts both user (KYC) and business (KYB) session IDs - the service resolves the ID across both session types. + email_address: Recipient shown in the preview. No email is sent. + nodes_to_resubmit: Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission. + email_language: Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsPreviewResubmissionResponse", + await self._client.request( + "post", + "/v3/session/{sessionId}/resubmission-preview/", + cast_to=SessionsPreviewResubmissionResponse, + path_params={"sessionId": session_id}, + auth="api_key", + body={ + "email_address": email_address, + "nodes_to_resubmit": nodes_to_resubmit, + "email_language": email_language, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class SessionsWithRawResponse: + def __init__(self, resource: Sessions) -> None: + self._resource = resource + self.create = to_raw_response_wrapper(resource.create) + self.retrieve_decision = to_raw_response_wrapper(resource.retrieve_decision) + self.delete = to_raw_response_wrapper(resource.delete) + self.generate_pdf = to_raw_response_wrapper(resource.generate_pdf) + self.update_status = to_raw_response_wrapper(resource.update_status) + self.list = to_raw_response_wrapper(resource.list) + self.share = to_raw_response_wrapper(resource.share) + self.import_shared = to_raw_response_wrapper(resource.import_shared) + self.batch_delete = to_raw_response_wrapper(resource.batch_delete) + self.retrieve_deletion_job = to_raw_response_wrapper(resource.retrieve_deletion_job) + self.update_data = to_raw_response_wrapper(resource.update_data) + self.update_feature_status = to_raw_response_wrapper(resource.update_feature_status) + self.update_aml_hit_status = to_raw_response_wrapper(resource.update_aml_hit_status) + self.bulk_update_aml_hit_status = to_raw_response_wrapper( + resource.bulk_update_aml_hit_status + ) + self.update_poa_data = to_raw_response_wrapper(resource.update_poa_data) + self.update_kyb_company_data = to_raw_response_wrapper(resource.update_kyb_company_data) + self.update_kyb_party = to_raw_response_wrapper(resource.update_kyb_party) + self.list_networks = to_raw_response_wrapper(resource.list_networks) + self.preview_resubmission = to_raw_response_wrapper(resource.preview_resubmission) + + @cached_property + def imports(self) -> SessionsImportsWithRawResponse: + from didit.resources.sessions.imports import SessionsImportsWithRawResponse + + return SessionsImportsWithRawResponse(self._resource.imports) + + @cached_property + def reviews(self) -> SessionsReviewsWithRawResponse: + from didit.resources.sessions.reviews import SessionsReviewsWithRawResponse + + return SessionsReviewsWithRawResponse(self._resource.reviews) + + +class AsyncSessionsWithRawResponse: + def __init__(self, resource: AsyncSessions) -> None: + self._resource = resource + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve_decision = async_to_raw_response_wrapper(resource.retrieve_decision) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.generate_pdf = async_to_raw_response_wrapper(resource.generate_pdf) + self.update_status = async_to_raw_response_wrapper(resource.update_status) + self.list = async_to_raw_response_wrapper(resource.list) + self.share = async_to_raw_response_wrapper(resource.share) + self.import_shared = async_to_raw_response_wrapper(resource.import_shared) + self.batch_delete = async_to_raw_response_wrapper(resource.batch_delete) + self.retrieve_deletion_job = async_to_raw_response_wrapper(resource.retrieve_deletion_job) + self.update_data = async_to_raw_response_wrapper(resource.update_data) + self.update_feature_status = async_to_raw_response_wrapper(resource.update_feature_status) + self.update_aml_hit_status = async_to_raw_response_wrapper(resource.update_aml_hit_status) + self.bulk_update_aml_hit_status = async_to_raw_response_wrapper( + resource.bulk_update_aml_hit_status + ) + self.update_poa_data = async_to_raw_response_wrapper(resource.update_poa_data) + self.update_kyb_company_data = async_to_raw_response_wrapper( + resource.update_kyb_company_data + ) + self.update_kyb_party = async_to_raw_response_wrapper(resource.update_kyb_party) + self.list_networks = async_to_raw_response_wrapper(resource.list_networks) + self.preview_resubmission = async_to_raw_response_wrapper(resource.preview_resubmission) + + @cached_property + def imports(self) -> AsyncSessionsImportsWithRawResponse: + from didit.resources.sessions.imports import AsyncSessionsImportsWithRawResponse + + return AsyncSessionsImportsWithRawResponse(self._resource.imports) + + @cached_property + def reviews(self) -> AsyncSessionsReviewsWithRawResponse: + from didit.resources.sessions.reviews import AsyncSessionsReviewsWithRawResponse + + return AsyncSessionsReviewsWithRawResponse(self._resource.reviews) + + +class SessionsWithStreamingResponse: + def __init__(self, resource: Sessions) -> None: + self._resource = resource + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve_decision = to_streamed_response_wrapper(resource.retrieve_decision) + self.delete = to_streamed_response_wrapper(resource.delete) + self.generate_pdf = to_streamed_response_wrapper(resource.generate_pdf) + self.update_status = to_streamed_response_wrapper(resource.update_status) + self.list = to_streamed_response_wrapper(resource.list) + self.share = to_streamed_response_wrapper(resource.share) + self.import_shared = to_streamed_response_wrapper(resource.import_shared) + self.batch_delete = to_streamed_response_wrapper(resource.batch_delete) + self.retrieve_deletion_job = to_streamed_response_wrapper(resource.retrieve_deletion_job) + self.update_data = to_streamed_response_wrapper(resource.update_data) + self.update_feature_status = to_streamed_response_wrapper(resource.update_feature_status) + self.update_aml_hit_status = to_streamed_response_wrapper(resource.update_aml_hit_status) + self.bulk_update_aml_hit_status = to_streamed_response_wrapper( + resource.bulk_update_aml_hit_status + ) + self.update_poa_data = to_streamed_response_wrapper(resource.update_poa_data) + self.update_kyb_company_data = to_streamed_response_wrapper( + resource.update_kyb_company_data + ) + self.update_kyb_party = to_streamed_response_wrapper(resource.update_kyb_party) + self.list_networks = to_streamed_response_wrapper(resource.list_networks) + self.preview_resubmission = to_streamed_response_wrapper(resource.preview_resubmission) + + @cached_property + def imports(self) -> SessionsImportsWithStreamingResponse: + from didit.resources.sessions.imports import SessionsImportsWithStreamingResponse + + return SessionsImportsWithStreamingResponse(self._resource.imports) + + @cached_property + def reviews(self) -> SessionsReviewsWithStreamingResponse: + from didit.resources.sessions.reviews import SessionsReviewsWithStreamingResponse + + return SessionsReviewsWithStreamingResponse(self._resource.reviews) + + +class AsyncSessionsWithStreamingResponse: + def __init__(self, resource: AsyncSessions) -> None: + self._resource = resource + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve_decision = async_to_streamed_response_wrapper(resource.retrieve_decision) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.generate_pdf = async_to_streamed_response_wrapper(resource.generate_pdf) + self.update_status = async_to_streamed_response_wrapper(resource.update_status) + self.list = async_to_streamed_response_wrapper(resource.list) + self.share = async_to_streamed_response_wrapper(resource.share) + self.import_shared = async_to_streamed_response_wrapper(resource.import_shared) + self.batch_delete = async_to_streamed_response_wrapper(resource.batch_delete) + self.retrieve_deletion_job = async_to_streamed_response_wrapper( + resource.retrieve_deletion_job + ) + self.update_data = async_to_streamed_response_wrapper(resource.update_data) + self.update_feature_status = async_to_streamed_response_wrapper( + resource.update_feature_status + ) + self.update_aml_hit_status = async_to_streamed_response_wrapper( + resource.update_aml_hit_status + ) + self.bulk_update_aml_hit_status = async_to_streamed_response_wrapper( + resource.bulk_update_aml_hit_status + ) + self.update_poa_data = async_to_streamed_response_wrapper(resource.update_poa_data) + self.update_kyb_company_data = async_to_streamed_response_wrapper( + resource.update_kyb_company_data + ) + self.update_kyb_party = async_to_streamed_response_wrapper(resource.update_kyb_party) + self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) + self.preview_resubmission = async_to_streamed_response_wrapper( + resource.preview_resubmission + ) + + @cached_property + def imports(self) -> AsyncSessionsImportsWithStreamingResponse: + from didit.resources.sessions.imports import AsyncSessionsImportsWithStreamingResponse + + return AsyncSessionsImportsWithStreamingResponse(self._resource.imports) + + @cached_property + def reviews(self) -> AsyncSessionsReviewsWithStreamingResponse: + from didit.resources.sessions.reviews import AsyncSessionsReviewsWithStreamingResponse + + return AsyncSessionsReviewsWithStreamingResponse(self._resource.reviews) diff --git a/src/didit/resources/sessions/imports.py b/src/didit/resources/sessions/imports.py new file mode 100644 index 0000000..8e59dd9 --- /dev/null +++ b/src/didit/resources/sessions/imports.py @@ -0,0 +1,459 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``sessions.imports`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, FileTypes, Headers, NotGiven, Query +from didit.types.components.session_import_error import SessionImportError +from didit.types.components.session_import_job import SessionImportJob +from didit.types.sessions.imports.list_errors_response import SessionsImportsListErrorsResponse + +__all__ = [ + "SessionsImports", + "AsyncSessionsImports", + "SessionsImportsWithRawResponse", + "AsyncSessionsImportsWithRawResponse", + "SessionsImportsWithStreamingResponse", + "AsyncSessionsImportsWithStreamingResponse", +] + + +class SessionsImports(SyncAPIResource): + """The ``sessions.imports`` resource of the Didit API. + + Methods: ``create``, ``list_errors``, ``retrieve``, ``retrieve_template``. + """ + + @cached_property + def with_raw_response(self) -> SessionsImportsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return SessionsImportsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsImportsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return SessionsImportsWithStreamingResponse(self) + + def create( + self, + *, + import_type: Literal[ + "user_verification", "business_verification", "status_rules", "transactions" + ] + | str + | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + source_format: Literal["csv", "ndjson"] | str | NotGiven = NOT_GIVEN, + source_file_url: str | NotGiven = NOT_GIVEN, + file: FileTypes | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionImportJob: + """Create a verification import job. + + Create an asynchronous import job for historical User Verification sessions, Business Verification sessions, transaction monitoring records, or workflow custom status rules. Use this to migrate from providers such as Sumsub, MetaMap, Veriff, Onfido, Persona, Trulioo, Jumio, Incode, iDenfy, or any other provider after transforming the export into Didit's canonical CSV/NDJSON format. Imports are disabled by default and must be enabled per organization by Didit. + + ``POST /v3/session/imports/`` + + Args: + import_type: What to import: historical User Verification sessions, historical Business Verification sessions, workflow custom status rules, or historical transaction monitoring records. + workflow_id: Workflow UUID that imported sessions or status rules should be attached to. Required for user_verification, business_verification, and status_rules. Omit for transactions. + source_format: Optional. Inferred from the URL's file extension when omitted. + source_file_url: Alternative to file upload. Must be an HTTPS URL resolving to a public address (private/loopback networks are rejected). Mutually exclusive with `file`. + file: Canonical CSV or NDJSON file. Use NDJSON for large imports. Maximum upload size 25 MB (use `source_file_url` for bigger files). Mutually exclusive with `source_file_url`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionImportJob", + self._client.request( + "post", + "/v3/session/imports/", + cast_to=SessionImportJob, + auth="api_key", + body={ + "import_type": import_type, + "workflow_id": workflow_id, + "source_format": source_format, + "source_file_url": source_file_url, + "file": file, + }, + multipart="auto", + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_template( + self, + *, + import_type: Literal[ + "user_verification", "business_verification", "status_rules", "transactions" + ] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download verification import template. + + Download the canonical CSV header for verification imports. The column set is determined by `import_type`. The first column of session templates is the idempotency key `external_id`. + + ``GET /v3/session/imports/template/`` + + Args: + import_type + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/session/imports/template/", + cast_to=bytes, + auth="api_key", + query={"import_type": import_type}, + accept="text/csv, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + import_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionImportJob: + """Retrieve a verification import job. + + Retrieve import progress counters and terminal status for one job. Poll until `status` is `COMPLETED` or `FAILED`; `failed_count` > 0 means some rows were rejected - list them via the errors endpoint. + + ``GET /v3/session/imports/{importId}/`` + + Args: + import_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionImportJob", + self._client.request( + "get", + "/v3/session/imports/{importId}/", + cast_to=SessionImportJob, + path_params={"importId": import_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_errors( + self, + import_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[SessionImportError]: + """List verification import row errors. + + List row-level errors for a verification import job, ordered by `row_number`. Use this endpoint to produce a corrected retry file containing only failed rows. + + ``GET /v3/session/imports/{importId}/errors/`` + + Args: + import_id + limit: Page size. Defaults to 50 when omitted. + offset: Zero-based offset of the first record to return. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[SessionImportError]", + self._client.request_page( + "get", + "/v3/session/imports/{importId}/errors/", + cast_to=SessionsImportsListErrorsResponse, + item_type=SessionImportError, + path_params={"importId": import_id}, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncSessionsImports(AsyncAPIResource): + """The ``sessions.imports`` resource of the Didit API. + + Methods: ``create``, ``list_errors``, ``retrieve``, ``retrieve_template``. + """ + + @cached_property + def with_raw_response(self) -> AsyncSessionsImportsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncSessionsImportsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsImportsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncSessionsImportsWithStreamingResponse(self) + + async def create( + self, + *, + import_type: Literal[ + "user_verification", "business_verification", "status_rules", "transactions" + ] + | str + | NotGiven = NOT_GIVEN, + workflow_id: str | NotGiven = NOT_GIVEN, + source_format: Literal["csv", "ndjson"] | str | NotGiven = NOT_GIVEN, + source_file_url: str | NotGiven = NOT_GIVEN, + file: FileTypes | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionImportJob: + """Create a verification import job. + + Create an asynchronous import job for historical User Verification sessions, Business Verification sessions, transaction monitoring records, or workflow custom status rules. Use this to migrate from providers such as Sumsub, MetaMap, Veriff, Onfido, Persona, Trulioo, Jumio, Incode, iDenfy, or any other provider after transforming the export into Didit's canonical CSV/NDJSON format. Imports are disabled by default and must be enabled per organization by Didit. + + ``POST /v3/session/imports/`` + + Args: + import_type: What to import: historical User Verification sessions, historical Business Verification sessions, workflow custom status rules, or historical transaction monitoring records. + workflow_id: Workflow UUID that imported sessions or status rules should be attached to. Required for user_verification, business_verification, and status_rules. Omit for transactions. + source_format: Optional. Inferred from the URL's file extension when omitted. + source_file_url: Alternative to file upload. Must be an HTTPS URL resolving to a public address (private/loopback networks are rejected). Mutually exclusive with `file`. + file: Canonical CSV or NDJSON file. Use NDJSON for large imports. Maximum upload size 25 MB (use `source_file_url` for bigger files). Mutually exclusive with `source_file_url`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionImportJob", + await self._client.request( + "post", + "/v3/session/imports/", + cast_to=SessionImportJob, + auth="api_key", + body={ + "import_type": import_type, + "workflow_id": workflow_id, + "source_format": source_format, + "source_file_url": source_file_url, + "file": file, + }, + multipart="auto", + file_fields=("file",), + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_template( + self, + *, + import_type: Literal[ + "user_verification", "business_verification", "status_rules", "transactions" + ] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download verification import template. + + Download the canonical CSV header for verification imports. The column set is determined by `import_type`. The first column of session templates is the idempotency key `external_id`. + + ``GET /v3/session/imports/template/`` + + Args: + import_type + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/session/imports/template/", + cast_to=bytes, + auth="api_key", + query={"import_type": import_type}, + accept="text/csv, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + import_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionImportJob: + """Retrieve a verification import job. + + Retrieve import progress counters and terminal status for one job. Poll until `status` is `COMPLETED` or `FAILED`; `failed_count` > 0 means some rows were rejected - list them via the errors endpoint. + + ``GET /v3/session/imports/{importId}/`` + + Args: + import_id + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionImportJob", + await self._client.request( + "get", + "/v3/session/imports/{importId}/", + cast_to=SessionImportJob, + path_params={"importId": import_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_errors( + self, + import_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[SessionImportError]: + """List verification import row errors. + + List row-level errors for a verification import job, ordered by `row_number`. Use this endpoint to produce a corrected retry file containing only failed rows. + + ``GET /v3/session/imports/{importId}/errors/`` + + Args: + import_id + limit: Page size. Defaults to 50 when omitted. + offset: Zero-based offset of the first record to return. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[SessionImportError]", + self._client.request_page( + "get", + "/v3/session/imports/{importId}/errors/", + cast_to=SessionsImportsListErrorsResponse, + item_type=SessionImportError, + path_params={"importId": import_id}, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class SessionsImportsWithRawResponse: + def __init__(self, resource: SessionsImports) -> None: + self._resource = resource + self.create = to_raw_response_wrapper(resource.create) + self.retrieve_template = to_raw_response_wrapper(resource.retrieve_template) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.list_errors = to_raw_response_wrapper(resource.list_errors) + + +class AsyncSessionsImportsWithRawResponse: + def __init__(self, resource: AsyncSessionsImports) -> None: + self._resource = resource + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve_template = async_to_raw_response_wrapper(resource.retrieve_template) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.list_errors = async_to_raw_response_wrapper(resource.list_errors) + + +class SessionsImportsWithStreamingResponse: + def __init__(self, resource: SessionsImports) -> None: + self._resource = resource + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve_template = to_streamed_response_wrapper(resource.retrieve_template) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.list_errors = to_streamed_response_wrapper(resource.list_errors) + + +class AsyncSessionsImportsWithStreamingResponse: + def __init__(self, resource: AsyncSessionsImports) -> None: + self._resource = resource + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve_template = async_to_streamed_response_wrapper(resource.retrieve_template) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.list_errors = async_to_streamed_response_wrapper(resource.list_errors) diff --git a/src/didit/resources/sessions/reviews.py b/src/didit/resources/sessions/reviews.py new file mode 100644 index 0000000..30493bd --- /dev/null +++ b/src/didit/resources/sessions/reviews.py @@ -0,0 +1,311 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``sessions.reviews`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.review_item import ReviewItem +from didit.types.sessions.reviews.create_response import SessionsReviewsCreateResponse +from didit.types.sessions.reviews.list_response import SessionsReviewsListResponse + +__all__ = [ + "SessionsReviews", + "AsyncSessionsReviews", + "SessionsReviewsWithRawResponse", + "AsyncSessionsReviewsWithRawResponse", + "SessionsReviewsWithStreamingResponse", + "AsyncSessionsReviewsWithStreamingResponse", +] + + +class SessionsReviews(SyncAPIResource): + """The ``sessions.reviews`` resource of the Didit API. + + Methods: ``create``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> SessionsReviewsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return SessionsReviewsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> SessionsReviewsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return SessionsReviewsWithStreamingResponse(self) + + def list( + self, + session_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[ReviewItem]: + """List session reviews and activity log. + + Return the audit trail / activity feed for a User Verification (KYC) session, newest first, in a paginated envelope (`count` / `next` / `previous` / `results`, 50 entries per page by default). + + Feed entries are written by every actor that touches the session: manual decisions made through `PATCH /v3/session/{session_id}/update-status/` (which records `previous_status` → `new_status`), Console reviewer activity (comments with `@email` mentions, KYC/POA data edits, file uploads/removals, tag changes, detail-page views), AML ongoing-monitoring matches, and notes appended via `POST` on this same path. Reading this feed is the API-side equivalent of the **Activity** panel in the Didit Console session view. + + Only User Verification (KYC) sessions are addressable here; Business Verification (KYB) session IDs return `404`. + + ``GET /v3/sessions/{session_id}/reviews/`` + + Args: + session_id: UUID of the User Verification (KYC) session. Must belong to the same application as the API key - cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`. + limit: Page size. Defaults to `50`. + offset: Number of entries to skip from the start of the (ordered) feed. Defaults to `0`. The `next` / `previous` URLs in the response carry the right values for walking the feed. + ordering: Sort order. The feed defaults to newest first (`-created_at`); pass `created_at` to read it oldest first. Unknown values are silently ignored (default order is used); other serializer field names are also accepted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[ReviewItem]", + self._client.request_page( + "get", + "/v3/sessions/{session_id}/reviews/", + cast_to=SessionsReviewsListResponse, + item_type=ReviewItem, + path_params={"session_id": session_id}, + auth="api_key", + query={"limit": limit, "offset": offset, "ordering": ordering}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + session_id: str, + *, + new_status: Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + | NotGiven = NOT_GIVEN, + comment: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsReviewsCreateResponse: + """Add a review note to a session's audit trail. + + Append a note to a User Verification (KYC) session's review / activity feed. **This endpoint only records an entry - it never changes the session.** The session's `status` is left untouched, no status-transition validation runs, and no webhook fires. To actually approve, decline, or re-open a session (which also writes a fully-attributed `STATUS_UPDATED` entry to this feed), use `PATCH /v3/session/{session_id}/update-status/` instead. + + Both body fields are optional and an empty JSON object is accepted. `new_status` is stored purely as a label on the entry and accepts any of the ten session status values (case-sensitive). `@email` mentions inside `comment` are stored as plain text only - they trigger no notifications on this endpoint, and the entry's `mentioned_emails` stays empty. + + Note on attribution: entries created here carry no actor information. In the feed they show `actor_display: "Unknown"`, `actor_type: "CONSOLE_USER"`, and `activity_type: "STATUS_UPDATED"` (storage defaults), and the `201` body's `user` field is `"Unknown"`. + + ``POST /v3/sessions/{session_id}/reviews/`` + + Args: + session_id: UUID of the User Verification (KYC) session. Must belong to the same application as the API key - cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`. + new_status: Optional status label to record on the entry. Accepts any session status value, case-sensitive (`"approved"` is rejected with `400`). Recording a status here does **not** change the session's actual status - use the update-status endpoint for that. + comment: Optional free-text note (no enforced length limit). Stored verbatim; `@email` mentions are not processed on this endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsReviewsCreateResponse", + self._client.request( + "post", + "/v3/sessions/{session_id}/reviews/", + cast_to=SessionsReviewsCreateResponse, + path_params={"session_id": session_id}, + auth="api_key", + body={"new_status": new_status, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncSessionsReviews(AsyncAPIResource): + """The ``sessions.reviews`` resource of the Didit API. + + Methods: ``create``, ``list``. + """ + + @cached_property + def with_raw_response(self) -> AsyncSessionsReviewsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncSessionsReviewsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncSessionsReviewsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncSessionsReviewsWithStreamingResponse(self) + + def list( + self, + session_id: str, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[ReviewItem]: + """List session reviews and activity log. + + Return the audit trail / activity feed for a User Verification (KYC) session, newest first, in a paginated envelope (`count` / `next` / `previous` / `results`, 50 entries per page by default). + + Feed entries are written by every actor that touches the session: manual decisions made through `PATCH /v3/session/{session_id}/update-status/` (which records `previous_status` → `new_status`), Console reviewer activity (comments with `@email` mentions, KYC/POA data edits, file uploads/removals, tag changes, detail-page views), AML ongoing-monitoring matches, and notes appended via `POST` on this same path. Reading this feed is the API-side equivalent of the **Activity** panel in the Didit Console session view. + + Only User Verification (KYC) sessions are addressable here; Business Verification (KYB) session IDs return `404`. + + ``GET /v3/sessions/{session_id}/reviews/`` + + Args: + session_id: UUID of the User Verification (KYC) session. Must belong to the same application as the API key - cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`. + limit: Page size. Defaults to `50`. + offset: Number of entries to skip from the start of the (ordered) feed. Defaults to `0`. The `next` / `previous` URLs in the response carry the right values for walking the feed. + ordering: Sort order. The feed defaults to newest first (`-created_at`); pass `created_at` to read it oldest first. Unknown values are silently ignored (default order is used); other serializer field names are also accepted. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[ReviewItem]", + self._client.request_page( + "get", + "/v3/sessions/{session_id}/reviews/", + cast_to=SessionsReviewsListResponse, + item_type=ReviewItem, + path_params={"session_id": session_id}, + auth="api_key", + query={"limit": limit, "offset": offset, "ordering": ordering}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + session_id: str, + *, + new_status: Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + | NotGiven = NOT_GIVEN, + comment: str | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SessionsReviewsCreateResponse: + """Add a review note to a session's audit trail. + + Append a note to a User Verification (KYC) session's review / activity feed. **This endpoint only records an entry - it never changes the session.** The session's `status` is left untouched, no status-transition validation runs, and no webhook fires. To actually approve, decline, or re-open a session (which also writes a fully-attributed `STATUS_UPDATED` entry to this feed), use `PATCH /v3/session/{session_id}/update-status/` instead. + + Both body fields are optional and an empty JSON object is accepted. `new_status` is stored purely as a label on the entry and accepts any of the ten session status values (case-sensitive). `@email` mentions inside `comment` are stored as plain text only - they trigger no notifications on this endpoint, and the entry's `mentioned_emails` stays empty. + + Note on attribution: entries created here carry no actor information. In the feed they show `actor_display: "Unknown"`, `actor_type: "CONSOLE_USER"`, and `activity_type: "STATUS_UPDATED"` (storage defaults), and the `201` body's `user` field is `"Unknown"`. + + ``POST /v3/sessions/{session_id}/reviews/`` + + Args: + session_id: UUID of the User Verification (KYC) session. Must belong to the same application as the API key - cross-application UUIDs, deleted sessions, and Business Verification (KYB) session IDs all return `404`. + new_status: Optional status label to record on the entry. Accepts any session status value, case-sensitive (`"approved"` is rejected with `400`). Recording a status here does **not** change the session's actual status - use the update-status endpoint for that. + comment: Optional free-text note (no enforced length limit). Stored verbatim; `@email` mentions are not processed on this endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SessionsReviewsCreateResponse", + await self._client.request( + "post", + "/v3/sessions/{session_id}/reviews/", + cast_to=SessionsReviewsCreateResponse, + path_params={"session_id": session_id}, + auth="api_key", + body={"new_status": new_status, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class SessionsReviewsWithRawResponse: + def __init__(self, resource: SessionsReviews) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + + +class AsyncSessionsReviewsWithRawResponse: + def __init__(self, resource: AsyncSessionsReviews) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + + +class SessionsReviewsWithStreamingResponse: + def __init__(self, resource: SessionsReviews) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + + +class AsyncSessionsReviewsWithStreamingResponse: + def __init__(self, resource: AsyncSessionsReviews) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) diff --git a/src/didit/resources/transactions/__init__.py b/src/didit/resources/transactions/__init__.py new file mode 100644 index 0000000..f5d5b42 --- /dev/null +++ b/src/didit/resources/transactions/__init__.py @@ -0,0 +1,782 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``transactions`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Mapping, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.transaction_detail import TransactionDetail +from didit.types.transactions.create_params import ( + TransactionsCreateParamsCounterparty, + TransactionsCreateParamsNetworkSnapshot, + TransactionsCreateParamsSubject, + TransactionsCreateParamsTransactionDetails, + TransactionsCreateParamsTravelRuleDetails, +) +from didit.types.transactions.create_sdk_token_response import TransactionsCreateSdkTokenResponse +from didit.types.transactions.list_networks_response import TransactionsListNetworksResponse +from didit.types.transactions.list_party_networks_response import ( + TransactionsListPartyNetworksResponse, +) +from didit.types.transactions.list_response import ( + TransactionsListResponse, + TransactionsListResponseResults, +) + +if TYPE_CHECKING: + from didit.resources.transactions.rules import ( + AsyncTransactionsRules, + AsyncTransactionsRulesWithRawResponse, + AsyncTransactionsRulesWithStreamingResponse, + TransactionsRules, + TransactionsRulesWithRawResponse, + TransactionsRulesWithStreamingResponse, + ) + from didit.resources.transactions.travel_rule import ( + AsyncTransactionsTravelRule, + AsyncTransactionsTravelRuleWithRawResponse, + AsyncTransactionsTravelRuleWithStreamingResponse, + TransactionsTravelRule, + TransactionsTravelRuleWithRawResponse, + TransactionsTravelRuleWithStreamingResponse, + ) + +__all__ = [ + "Transactions", + "AsyncTransactions", + "TransactionsWithRawResponse", + "AsyncTransactionsWithRawResponse", + "TransactionsWithStreamingResponse", + "AsyncTransactionsWithStreamingResponse", +] + + +class Transactions(SyncAPIResource): + """The ``transactions`` resource of the Didit API. + + Methods: ``create``, ``create_sdk_token``, ``list``, ``list_networks``, ``list_party_networks``, ``retrieve``. + + Nested resources: ``rules``, ``travel_rule``. + """ + + @cached_property + def rules(self) -> TransactionsRules: + from didit.resources.transactions.rules import TransactionsRules + + return TransactionsRules(self._client) + + @cached_property + def travel_rule(self) -> TransactionsTravelRule: + from didit.resources.transactions.travel_rule import TransactionsTravelRule + + return TransactionsTravelRule(self._client) + + @cached_property + def with_raw_response(self) -> TransactionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TransactionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TransactionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TransactionsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TransactionsListResponseResults]: + """List transactions. + + Paginated list of monitored transactions, newest first (`txn_date` desc). `count` is capped at 100; use `total_transactions` for the true row count. This endpoint does not support filtering or search parameters. + + ``GET /v3/transactions/`` + + Args: + limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. + offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TransactionsListResponseResults]", + self._client.request_page( + "get", + "/v3/transactions/", + cast_to=TransactionsListResponse, + item_type=TransactionsListResponseResults, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + transaction_id: str, + transaction_category: Literal[ + "finance", + "kyc", + "travel_rule", + "user_event", + "audit_trail_event", + "gambling_bet", + "gambling_limit_change", + "gambling_bonus_change", + "travelRule", + ] + | str, + transaction_details: TransactionsCreateParamsTransactionDetails, + subject: TransactionsCreateParamsSubject, + transaction_at: str | _dt.datetime | NotGiven = NOT_GIVEN, + time_zone: str | NotGiven = NOT_GIVEN, + counterparty: TransactionsCreateParamsCounterparty | NotGiven = NOT_GIVEN, + custom_properties: Mapping[str, object] | NotGiven = NOT_GIVEN, + travel_rule_details: TransactionsCreateParamsTravelRuleDetails | NotGiven = NOT_GIVEN, + network_snapshot: TransactionsCreateParamsNetworkSnapshot | NotGiven = NOT_GIVEN, + include_crypto_screening: bool | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionDetail: + """Create transaction. + + Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success (sandbox applications are not billed and do not emit webhooks). + + ``POST /v3/transactions/`` + + Args: + transaction_id: Your unique identifier for this transaction (max 128 characters). + transaction_category: Category of the transaction. CamelCase aliases (`travelRule`, `userPlatformEvent`, `gamblingBet`, …) are also accepted; the stored/echoed value is camelCase for multi-word categories. + transaction_details: Core financial details of the transaction. + subject: The subject (applicant) of the transaction. + transaction_at: When the transaction occurred. Defaults to now if omitted. + time_zone: IANA time zone identifier, e.g. `Europe/Madrid`. + counterparty: The counterparty of the transaction (optional). + custom_properties: Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`. + travel_rule_details: Travel Rule compliance metadata. + network_snapshot: Pre-computed network graph snapshot. + include_crypto_screening: Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: "crypto"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionDetail", + self._client.request( + "post", + "/v3/transactions/", + cast_to=TransactionDetail, + auth="api_key", + body={ + "transaction_id": transaction_id, + "transaction_category": transaction_category, + "transaction_details": transaction_details, + "subject": subject, + "transaction_at": transaction_at, + "time_zone": time_zone, + "counterparty": counterparty, + "custom_properties": custom_properties, + "travel_rule_details": travel_rule_details, + "network_snapshot": network_snapshot, + "include_crypto_screening": include_crypto_screening, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + transaction_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionDetail: + """Get transaction. + + Retrieve the full monitoring record for one transaction: parties, score/severity, rule runs, alerts, Travel Rule outcome, remediation session. + + ``GET /v3/transactions/{transaction_id}/`` + + Args: + transaction_id: Didit-stable transaction UUID (the `uuid` returned from create / list, **not** your `txn_id`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionDetail", + self._client.request( + "get", + "/v3/transactions/{transaction_id}/", + cast_to=TransactionDetail, + path_params={"transaction_id": transaction_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_sdk_token( + self, + *, + vendor_data: str, + ttl_seconds: int | NotGiven = NOT_GIVEN, + max_uses: int | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionsCreateSdkTokenResponse: + """Mint an SDK transaction token. + + Mint a short-lived scoped token your app passes to the Didit SDKs so they can submit transactions directly from the end-user's device via `POST /v1/transactions/`. The token is bound to one `vendor_data` (your user id): every transaction submitted with it has its subject identity enforced server-side from that binding, so a tampered client cannot submit on behalf of another user. Mint tokens from your backend - never ship the API key itself in an app. camelCase aliases (`vendorData`, `ttlSeconds`, `maxUses`) are also accepted. + + ``POST /v3/transactions/sdk-token/`` + + Args: + vendor_data: Your internal identifier for the end user this token is scoped to. Enforced as the subject identity of every transaction submitted with the token. + ttl_seconds: Token lifetime in seconds. Defaults to 900 (15 minutes); maximum 86400 (24 hours). + max_uses: Maximum number of successful submissions allowed with this token. Omit or null for unlimited uses within the TTL. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionsCreateSdkTokenResponse", + self._client.request( + "post", + "/v3/transactions/sdk-token/", + cast_to=TransactionsCreateSdkTokenResponse, + auth="api_key", + body={"vendor_data": vendor_data, "ttl_seconds": ttl_seconds, "max_uses": max_uses}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_networks( + self, + transaction_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionsListNetworksResponse: + """List networks for a transaction. + + List the networks the transaction's backing KYC/KYB subject belongs to (resolved through the transaction's session or business session). + + ``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/`` + + Args: + transaction_uuid: Transaction UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionsListNetworksResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/", + cast_to=TransactionsListNetworksResponse, + path_params={"transaction_uuid": transaction_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_party_networks( + self, + transaction_uuid: str, + transaction_party_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionsListPartyNetworksResponse: + """List networks for a transaction party. + + List the networks a transaction party (remitter, beneficiary, or counterparty) belongs to, resolved through the party itself rather than through the transaction's backing session. + + ``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/`` + + Args: + transaction_uuid: Transaction UUID. + transaction_party_uuid: Transaction party UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionsListPartyNetworksResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/", + cast_to=TransactionsListPartyNetworksResponse, + path_params={ + "transaction_uuid": transaction_uuid, + "transaction_party_uuid": transaction_party_uuid, + }, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTransactions(AsyncAPIResource): + """The ``transactions`` resource of the Didit API. + + Methods: ``create``, ``create_sdk_token``, ``list``, ``list_networks``, ``list_party_networks``, ``retrieve``. + + Nested resources: ``rules``, ``travel_rule``. + """ + + @cached_property + def rules(self) -> AsyncTransactionsRules: + from didit.resources.transactions.rules import AsyncTransactionsRules + + return AsyncTransactionsRules(self._client) + + @cached_property + def travel_rule(self) -> AsyncTransactionsTravelRule: + from didit.resources.transactions.travel_rule import AsyncTransactionsTravelRule + + return AsyncTransactionsTravelRule(self._client) + + @cached_property + def with_raw_response(self) -> AsyncTransactionsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTransactionsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTransactionsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTransactionsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TransactionsListResponseResults]: + """List transactions. + + Paginated list of monitored transactions, newest first (`txn_date` desc). `count` is capped at 100; use `total_transactions` for the true row count. This endpoint does not support filtering or search parameters. + + ``GET /v3/transactions/`` + + Args: + limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. + offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TransactionsListResponseResults]", + self._client.request_page( + "get", + "/v3/transactions/", + cast_to=TransactionsListResponse, + item_type=TransactionsListResponseResults, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + transaction_id: str, + transaction_category: Literal[ + "finance", + "kyc", + "travel_rule", + "user_event", + "audit_trail_event", + "gambling_bet", + "gambling_limit_change", + "gambling_bonus_change", + "travelRule", + ] + | str, + transaction_details: TransactionsCreateParamsTransactionDetails, + subject: TransactionsCreateParamsSubject, + transaction_at: str | _dt.datetime | NotGiven = NOT_GIVEN, + time_zone: str | NotGiven = NOT_GIVEN, + counterparty: TransactionsCreateParamsCounterparty | NotGiven = NOT_GIVEN, + custom_properties: Mapping[str, object] | NotGiven = NOT_GIVEN, + travel_rule_details: TransactionsCreateParamsTravelRuleDetails | NotGiven = NOT_GIVEN, + network_snapshot: TransactionsCreateParamsNetworkSnapshot | NotGiven = NOT_GIVEN, + include_crypto_screening: bool | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionDetail: + """Create transaction. + + Submit a transaction for synchronous AML/risk monitoring. The transaction is created `APPROVED`, then your rules (and crypto screening, when applicable) run inline and may flip the returned `status` to `IN_REVIEW` or `DECLINED`. **`transaction_id` is your idempotency key** (max 128 chars, unique per app); reusing it returns 400. A `transaction.created` webhook is dispatched on success (sandbox applications are not billed and do not emit webhooks). + + ``POST /v3/transactions/`` + + Args: + transaction_id: Your unique identifier for this transaction (max 128 characters). + transaction_category: Category of the transaction. CamelCase aliases (`travelRule`, `userPlatformEvent`, `gamblingBet`, …) are also accepted; the stored/echoed value is camelCase for multi-word categories. + transaction_details: Core financial details of the transaction. + subject: The subject (applicant) of the transaction. + transaction_at: When the transaction occurred. Defaults to now if omitted. + time_zone: IANA time zone identifier, e.g. `Europe/Madrid`. + counterparty: The counterparty of the transaction (optional). + custom_properties: Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`. + travel_rule_details: Travel Rule compliance metadata. + network_snapshot: Pre-computed network graph snapshot. + include_crypto_screening: Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: "crypto"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionDetail", + await self._client.request( + "post", + "/v3/transactions/", + cast_to=TransactionDetail, + auth="api_key", + body={ + "transaction_id": transaction_id, + "transaction_category": transaction_category, + "transaction_details": transaction_details, + "subject": subject, + "transaction_at": transaction_at, + "time_zone": time_zone, + "counterparty": counterparty, + "custom_properties": custom_properties, + "travel_rule_details": travel_rule_details, + "network_snapshot": network_snapshot, + "include_crypto_screening": include_crypto_screening, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + transaction_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionDetail: + """Get transaction. + + Retrieve the full monitoring record for one transaction: parties, score/severity, rule runs, alerts, Travel Rule outcome, remediation session. + + ``GET /v3/transactions/{transaction_id}/`` + + Args: + transaction_id: Didit-stable transaction UUID (the `uuid` returned from create / list, **not** your `txn_id`). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionDetail", + await self._client.request( + "get", + "/v3/transactions/{transaction_id}/", + cast_to=TransactionDetail, + path_params={"transaction_id": transaction_id}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_sdk_token( + self, + *, + vendor_data: str, + ttl_seconds: int | NotGiven = NOT_GIVEN, + max_uses: int | None | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionsCreateSdkTokenResponse: + """Mint an SDK transaction token. + + Mint a short-lived scoped token your app passes to the Didit SDKs so they can submit transactions directly from the end-user's device via `POST /v1/transactions/`. The token is bound to one `vendor_data` (your user id): every transaction submitted with it has its subject identity enforced server-side from that binding, so a tampered client cannot submit on behalf of another user. Mint tokens from your backend - never ship the API key itself in an app. camelCase aliases (`vendorData`, `ttlSeconds`, `maxUses`) are also accepted. + + ``POST /v3/transactions/sdk-token/`` + + Args: + vendor_data: Your internal identifier for the end user this token is scoped to. Enforced as the subject identity of every transaction submitted with the token. + ttl_seconds: Token lifetime in seconds. Defaults to 900 (15 minutes); maximum 86400 (24 hours). + max_uses: Maximum number of successful submissions allowed with this token. Omit or null for unlimited uses within the TTL. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionsCreateSdkTokenResponse", + await self._client.request( + "post", + "/v3/transactions/sdk-token/", + cast_to=TransactionsCreateSdkTokenResponse, + auth="api_key", + body={"vendor_data": vendor_data, "ttl_seconds": ttl_seconds, "max_uses": max_uses}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_networks( + self, + transaction_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionsListNetworksResponse: + """List networks for a transaction. + + List the networks the transaction's backing KYC/KYB subject belongs to (resolved through the transaction's session or business session). + + ``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/`` + + Args: + transaction_uuid: Transaction UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionsListNetworksResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/", + cast_to=TransactionsListNetworksResponse, + path_params={"transaction_uuid": transaction_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_party_networks( + self, + transaction_uuid: str, + transaction_party_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionsListPartyNetworksResponse: + """List networks for a transaction party. + + List the networks a transaction party (remitter, beneficiary, or counterparty) belongs to, resolved through the party itself rather than through the transaction's backing session. + + ``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/`` + + Args: + transaction_uuid: Transaction UUID. + transaction_party_uuid: Transaction party UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionsListPartyNetworksResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/", + cast_to=TransactionsListPartyNetworksResponse, + path_params={ + "transaction_uuid": transaction_uuid, + "transaction_party_uuid": transaction_party_uuid, + }, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TransactionsWithRawResponse: + def __init__(self, resource: Transactions) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.create_sdk_token = to_raw_response_wrapper(resource.create_sdk_token) + self.list_networks = to_raw_response_wrapper(resource.list_networks) + self.list_party_networks = to_raw_response_wrapper(resource.list_party_networks) + + @cached_property + def rules(self) -> TransactionsRulesWithRawResponse: + from didit.resources.transactions.rules import TransactionsRulesWithRawResponse + + return TransactionsRulesWithRawResponse(self._resource.rules) + + @cached_property + def travel_rule(self) -> TransactionsTravelRuleWithRawResponse: + from didit.resources.transactions.travel_rule import TransactionsTravelRuleWithRawResponse + + return TransactionsTravelRuleWithRawResponse(self._resource.travel_rule) + + +class AsyncTransactionsWithRawResponse: + def __init__(self, resource: AsyncTransactions) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.create_sdk_token = async_to_raw_response_wrapper(resource.create_sdk_token) + self.list_networks = async_to_raw_response_wrapper(resource.list_networks) + self.list_party_networks = async_to_raw_response_wrapper(resource.list_party_networks) + + @cached_property + def rules(self) -> AsyncTransactionsRulesWithRawResponse: + from didit.resources.transactions.rules import AsyncTransactionsRulesWithRawResponse + + return AsyncTransactionsRulesWithRawResponse(self._resource.rules) + + @cached_property + def travel_rule(self) -> AsyncTransactionsTravelRuleWithRawResponse: + from didit.resources.transactions.travel_rule import ( + AsyncTransactionsTravelRuleWithRawResponse, + ) + + return AsyncTransactionsTravelRuleWithRawResponse(self._resource.travel_rule) + + +class TransactionsWithStreamingResponse: + def __init__(self, resource: Transactions) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.create_sdk_token = to_streamed_response_wrapper(resource.create_sdk_token) + self.list_networks = to_streamed_response_wrapper(resource.list_networks) + self.list_party_networks = to_streamed_response_wrapper(resource.list_party_networks) + + @cached_property + def rules(self) -> TransactionsRulesWithStreamingResponse: + from didit.resources.transactions.rules import TransactionsRulesWithStreamingResponse + + return TransactionsRulesWithStreamingResponse(self._resource.rules) + + @cached_property + def travel_rule(self) -> TransactionsTravelRuleWithStreamingResponse: + from didit.resources.transactions.travel_rule import ( + TransactionsTravelRuleWithStreamingResponse, + ) + + return TransactionsTravelRuleWithStreamingResponse(self._resource.travel_rule) + + +class AsyncTransactionsWithStreamingResponse: + def __init__(self, resource: AsyncTransactions) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.create_sdk_token = async_to_streamed_response_wrapper(resource.create_sdk_token) + self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) + self.list_party_networks = async_to_streamed_response_wrapper(resource.list_party_networks) + + @cached_property + def rules(self) -> AsyncTransactionsRulesWithStreamingResponse: + from didit.resources.transactions.rules import AsyncTransactionsRulesWithStreamingResponse + + return AsyncTransactionsRulesWithStreamingResponse(self._resource.rules) + + @cached_property + def travel_rule(self) -> AsyncTransactionsTravelRuleWithStreamingResponse: + from didit.resources.transactions.travel_rule import ( + AsyncTransactionsTravelRuleWithStreamingResponse, + ) + + return AsyncTransactionsTravelRuleWithStreamingResponse(self._resource.travel_rule) diff --git a/src/didit/resources/transactions/rules.py b/src/didit/resources/transactions/rules.py new file mode 100644 index 0000000..867bca6 --- /dev/null +++ b/src/didit/resources/transactions/rules.py @@ -0,0 +1,1032 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``transactions.rules`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.transaction_rule_action import TransactionRuleActionParam +from didit.types.components.transaction_rule_aggregation import TransactionRuleAggregationParam +from didit.types.components.transaction_rule_backtest_result import TransactionRuleBacktestResult +from didit.types.components.transaction_rule_condition import TransactionRuleConditionParam +from didit.types.components.transaction_rule_detail import TransactionRuleDetail +from didit.types.components.transaction_rule_install_result import TransactionRuleInstallResult +from didit.types.components.transaction_rule_library_entry import TransactionRuleLibraryEntry +from didit.types.components.transaction_rule_scope import TransactionRuleScopeParam +from didit.types.transactions.rules.list_library_response import ( + TransactionsRulesListLibraryResponse, +) +from didit.types.transactions.rules.list_response import TransactionsRulesListResponse + +__all__ = [ + "TransactionsRules", + "AsyncTransactionsRules", + "TransactionsRulesWithRawResponse", + "AsyncTransactionsRulesWithRawResponse", + "TransactionsRulesWithStreamingResponse", + "AsyncTransactionsRulesWithStreamingResponse", +] + + +class TransactionsRules(SyncAPIResource): + """The ``transactions.rules`` resource of the Didit API. + + Methods: ``backtest``, ``create``, ``delete``, ``install_library``, ``list``, ``list_library``, ``retrieve``, ``uninstall_library``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> TransactionsRulesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TransactionsRulesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TransactionsRulesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TransactionsRulesWithStreamingResponse(self) + + def list( + self, + *, + source: Literal["PRESET", "CUSTOM"] | str | NotGiven = NOT_GIVEN, + category: str | NotGiven = NOT_GIVEN, + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str | NotGiven = NOT_GIVEN, + bundle: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TransactionRuleDetail]: + """List rules. + + List the application's transaction-monitoring rules: your custom rules plus any library presets you have installed. Filter by source, category, mode, bundle or free-text search; order with `ordering` (run_count, approved_pct, reviewed_pct, declined_pct, latest_triggered_at, title, created_at, source, each with an optional `-` prefix). + + ``GET /v3/transactions/rules/`` + + Args: + source: PRESET or CUSTOM + category: Filter by rule category + mode: ACTIVE, DISABLED or TEST + bundle: Filter presets by bundle + search: Case-insensitive match against title and description + ordering: Comma-separated ordering fields, default source,-run_count + limit: Page size + offset: Page offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TransactionRuleDetail]", + self._client.request_page( + "get", + "/v3/transactions/rules/", + cast_to=TransactionsRulesListResponse, + item_type=TransactionRuleDetail, + auth="api_key", + query={ + "source": source, + "category": category, + "mode": mode, + "bundle": bundle, + "search": search, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + title: str, + category: str, + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str, + description: str | None | NotGiven = NOT_GIVEN, + evaluation_mode: Literal["ALL", "ANY"] | str | NotGiven = NOT_GIVEN, + scope: TransactionRuleScopeParam | NotGiven = NOT_GIVEN, + conditions: Sequence[TransactionRuleConditionParam] | NotGiven = NOT_GIVEN, + aggregation: Sequence[TransactionRuleAggregationParam] | NotGiven = NOT_GIVEN, + actions: Sequence[TransactionRuleActionParam] | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleDetail: + """Create rule. + + Create a CUSTOM transaction-monitoring rule. Titles are unique per application (duplicate -> 400). A `change_status` action targeting `AWAITING_USER` must carry `workflow_id`. Rules created in ACTIVE mode apply to new transactions immediately - the recommended workflow is create in TEST mode, backtest, then switch mode to ACTIVE. + + ``POST /v3/transactions/rules/`` + + Args: + title: Rule name. Unique per application among non-deleted rules; duplicates return 400. + category: Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce. + mode: ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped. + description + evaluation_mode: Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index. + scope: Which transactions the rule applies to. Only these three keys are accepted. + conditions + aggregation + actions + metadata: Free-form metadata stored on the rule. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleDetail", + self._client.request( + "post", + "/v3/transactions/rules/", + cast_to=TransactionRuleDetail, + auth="api_key", + body={ + "title": title, + "category": category, + "mode": mode, + "description": description, + "evaluation_mode": evaluation_mode, + "scope": scope, + "conditions": conditions, + "aggregation": aggregation, + "actions": actions, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + rule_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleDetail: + """Get rule. + + Retrieve one rule with its full configuration and execution counters. + + ``GET /v3/transactions/rules/{rule_uuid}/`` + + Args: + rule_uuid: The rule's uuid from list or create. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleDetail", + self._client.request( + "get", + "/v3/transactions/rules/{rule_uuid}/", + cast_to=TransactionRuleDetail, + path_params={"rule_uuid": rule_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + rule_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete rule. + + Delete a CUSTOM rule. Deletion is immediate and permanent: the rule stops evaluating for new transactions and disappears from listings, while past rule runs on already-evaluated transactions keep their recorded results. Library rules cannot be deleted - uninstall them via the install endpoint instead (they can always be reinstalled from the library). + + ``DELETE /v3/transactions/rules/{rule_uuid}/`` + + Args: + rule_uuid: The rule's uuid from list or create. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/transactions/rules/{rule_uuid}/", + cast_to=None, + path_params={"rule_uuid": rule_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + rule_uuid: str, + *, + title: str | NotGiven = NOT_GIVEN, + description: str | None | NotGiven = NOT_GIVEN, + category: str | NotGiven = NOT_GIVEN, + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str | NotGiven = NOT_GIVEN, + evaluation_mode: Literal["ALL", "ANY"] | str | NotGiven = NOT_GIVEN, + scope: TransactionRuleScopeParam | NotGiven = NOT_GIVEN, + conditions: Sequence[TransactionRuleConditionParam] | NotGiven = NOT_GIVEN, + aggregation: Sequence[TransactionRuleAggregationParam] | NotGiven = NOT_GIVEN, + actions: Sequence[TransactionRuleActionParam] | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleDetail: + """Update rule. + + Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. + + ``PATCH /v3/transactions/rules/{rule_uuid}/`` + + Args: + rule_uuid: The rule's uuid from list or create. + title: Rule name. Unique per application among non-deleted rules; duplicates return 400. + description + category: Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce. + mode: ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped. + evaluation_mode: Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index. + scope: Which transactions the rule applies to. Only these three keys are accepted. + conditions + aggregation + actions + metadata: Free-form metadata stored on the rule. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleDetail", + self._client.request( + "patch", + "/v3/transactions/rules/{rule_uuid}/", + cast_to=TransactionRuleDetail, + path_params={"rule_uuid": rule_uuid}, + auth="api_key", + body={ + "title": title, + "description": description, + "category": category, + "mode": mode, + "evaluation_mode": evaluation_mode, + "scope": scope, + "conditions": conditions, + "aggregation": aggregation, + "actions": actions, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def backtest( + self, + *, + conditions: Sequence[TransactionRuleConditionParam] | NotGiven = NOT_GIVEN, + aggregation: Sequence[TransactionRuleAggregationParam] | NotGiven = NOT_GIVEN, + evaluation_mode: Literal["ALL", "ANY"] | str | NotGiven = NOT_GIVEN, + scope: TransactionRuleScopeParam | NotGiven = NOT_GIVEN, + period_days: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleBacktestResult: + """Backtest rule configuration. + + Evaluate a hypothetical rule configuration against your recent transactions (most recent first, up to the backtest cap) without creating a rule or touching any transaction. Use it before activating a rule to see how often it would have matched and how many subjects it would have affected. + + ``POST /v3/transactions/rules/backtest/`` + + Args: + conditions + aggregation + evaluation_mode + scope: Which transactions the rule applies to. Only these three keys are accepted. + period_days + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleBacktestResult", + self._client.request( + "post", + "/v3/transactions/rules/backtest/", + cast_to=TransactionRuleBacktestResult, + auth="api_key", + body={ + "conditions": conditions, + "aggregation": aggregation, + "evaluation_mode": evaluation_mode, + "scope": scope, + "period_days": period_days, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_library( + self, + *, + bundle: str | NotGiven = NOT_GIVEN, + category: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[TransactionRuleLibraryEntry]: + """List rule library. + + Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. + + ``GET /v3/transactions/rules/library/`` + + Args: + bundle: Filter by bundle, e.g. finance, aml_ctf, crypto_monitoring, travel_rule + category: Filter by category + search: Case-insensitive match against title and description + limit: Page size + offset: Page offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[TransactionRuleLibraryEntry]", + self._client.request_page( + "get", + "/v3/transactions/rules/library/", + cast_to=TransactionsRulesListLibraryResponse, + item_type=TransactionRuleLibraryEntry, + auth="api_key", + query={ + "bundle": bundle, + "category": category, + "search": search, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def install_library( + self, + *, + library_keys: Sequence[str] | NotGiven = NOT_GIVEN, + bundle: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleInstallResult: + """Install library rules. + + Install one or more presets onto the application, by `library_keys` or a whole `bundle`. Installed presets evaluate immediately in their default mode; already-installed keys are skipped. + + ``POST /v3/transactions/rules/install/`` + + Args: + library_keys: Specific presets to install or uninstall. + bundle: Install or uninstall every preset in this bundle instead. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleInstallResult", + self._client.request( + "post", + "/v3/transactions/rules/install/", + cast_to=TransactionRuleInstallResult, + auth="api_key", + body={"library_keys": library_keys, "bundle": bundle}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def uninstall_library( + self, + *, + library_keys: Sequence[str] | NotGiven = NOT_GIVEN, + bundle: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleInstallResult: + """Uninstall library rules. + + Remove installed presets from the application, by `library_keys` or a whole `bundle`. Uninstalled presets stop evaluating immediately and can be reinstalled from the library at any time. + + ``DELETE /v3/transactions/rules/install/`` + + Args: + library_keys: Specific presets to install or uninstall. + bundle: Install or uninstall every preset in this bundle instead. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleInstallResult", + self._client.request( + "delete", + "/v3/transactions/rules/install/", + cast_to=TransactionRuleInstallResult, + auth="api_key", + body={"library_keys": library_keys, "bundle": bundle}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTransactionsRules(AsyncAPIResource): + """The ``transactions.rules`` resource of the Didit API. + + Methods: ``backtest``, ``create``, ``delete``, ``install_library``, ``list``, ``list_library``, ``retrieve``, ``uninstall_library``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTransactionsRulesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTransactionsRulesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTransactionsRulesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTransactionsRulesWithStreamingResponse(self) + + def list( + self, + *, + source: Literal["PRESET", "CUSTOM"] | str | NotGiven = NOT_GIVEN, + category: str | NotGiven = NOT_GIVEN, + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str | NotGiven = NOT_GIVEN, + bundle: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + ordering: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TransactionRuleDetail]: + """List rules. + + List the application's transaction-monitoring rules: your custom rules plus any library presets you have installed. Filter by source, category, mode, bundle or free-text search; order with `ordering` (run_count, approved_pct, reviewed_pct, declined_pct, latest_triggered_at, title, created_at, source, each with an optional `-` prefix). + + ``GET /v3/transactions/rules/`` + + Args: + source: PRESET or CUSTOM + category: Filter by rule category + mode: ACTIVE, DISABLED or TEST + bundle: Filter presets by bundle + search: Case-insensitive match against title and description + ordering: Comma-separated ordering fields, default source,-run_count + limit: Page size + offset: Page offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TransactionRuleDetail]", + self._client.request_page( + "get", + "/v3/transactions/rules/", + cast_to=TransactionsRulesListResponse, + item_type=TransactionRuleDetail, + auth="api_key", + query={ + "source": source, + "category": category, + "mode": mode, + "bundle": bundle, + "search": search, + "ordering": ordering, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + title: str, + category: str, + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str, + description: str | None | NotGiven = NOT_GIVEN, + evaluation_mode: Literal["ALL", "ANY"] | str | NotGiven = NOT_GIVEN, + scope: TransactionRuleScopeParam | NotGiven = NOT_GIVEN, + conditions: Sequence[TransactionRuleConditionParam] | NotGiven = NOT_GIVEN, + aggregation: Sequence[TransactionRuleAggregationParam] | NotGiven = NOT_GIVEN, + actions: Sequence[TransactionRuleActionParam] | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleDetail: + """Create rule. + + Create a CUSTOM transaction-monitoring rule. Titles are unique per application (duplicate -> 400). A `change_status` action targeting `AWAITING_USER` must carry `workflow_id`. Rules created in ACTIVE mode apply to new transactions immediately - the recommended workflow is create in TEST mode, backtest, then switch mode to ACTIVE. + + ``POST /v3/transactions/rules/`` + + Args: + title: Rule name. Unique per application among non-deleted rules; duplicates return 400. + category: Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce. + mode: ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped. + description + evaluation_mode: Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index. + scope: Which transactions the rule applies to. Only these three keys are accepted. + conditions + aggregation + actions + metadata: Free-form metadata stored on the rule. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleDetail", + await self._client.request( + "post", + "/v3/transactions/rules/", + cast_to=TransactionRuleDetail, + auth="api_key", + body={ + "title": title, + "category": category, + "mode": mode, + "description": description, + "evaluation_mode": evaluation_mode, + "scope": scope, + "conditions": conditions, + "aggregation": aggregation, + "actions": actions, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + rule_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleDetail: + """Get rule. + + Retrieve one rule with its full configuration and execution counters. + + ``GET /v3/transactions/rules/{rule_uuid}/`` + + Args: + rule_uuid: The rule's uuid from list or create. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleDetail", + await self._client.request( + "get", + "/v3/transactions/rules/{rule_uuid}/", + cast_to=TransactionRuleDetail, + path_params={"rule_uuid": rule_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + rule_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete rule. + + Delete a CUSTOM rule. Deletion is immediate and permanent: the rule stops evaluating for new transactions and disappears from listings, while past rule runs on already-evaluated transactions keep their recorded results. Library rules cannot be deleted - uninstall them via the install endpoint instead (they can always be reinstalled from the library). + + ``DELETE /v3/transactions/rules/{rule_uuid}/`` + + Args: + rule_uuid: The rule's uuid from list or create. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/transactions/rules/{rule_uuid}/", + cast_to=None, + path_params={"rule_uuid": rule_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + rule_uuid: str, + *, + title: str | NotGiven = NOT_GIVEN, + description: str | None | NotGiven = NOT_GIVEN, + category: str | NotGiven = NOT_GIVEN, + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str | NotGiven = NOT_GIVEN, + evaluation_mode: Literal["ALL", "ANY"] | str | NotGiven = NOT_GIVEN, + scope: TransactionRuleScopeParam | NotGiven = NOT_GIVEN, + conditions: Sequence[TransactionRuleConditionParam] | NotGiven = NOT_GIVEN, + aggregation: Sequence[TransactionRuleAggregationParam] | NotGiven = NOT_GIVEN, + actions: Sequence[TransactionRuleActionParam] | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleDetail: + """Update rule. + + Partially update a rule. Custom rules accept every configuration field. Library (PRESET) rules accept only `mode` - use it to activate, disable, or switch a preset to TEST; any other field returns 400. + + ``PATCH /v3/transactions/rules/{rule_uuid}/`` + + Args: + rule_uuid: The rule's uuid from list or create. + title: Rule name. Unique per application among non-deleted rules; duplicates return 400. + description + category: Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce. + mode: ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped. + evaluation_mode: Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index. + scope: Which transactions the rule applies to. Only these three keys are accepted. + conditions + aggregation + actions + metadata: Free-form metadata stored on the rule. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleDetail", + await self._client.request( + "patch", + "/v3/transactions/rules/{rule_uuid}/", + cast_to=TransactionRuleDetail, + path_params={"rule_uuid": rule_uuid}, + auth="api_key", + body={ + "title": title, + "description": description, + "category": category, + "mode": mode, + "evaluation_mode": evaluation_mode, + "scope": scope, + "conditions": conditions, + "aggregation": aggregation, + "actions": actions, + "metadata": metadata, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def backtest( + self, + *, + conditions: Sequence[TransactionRuleConditionParam] | NotGiven = NOT_GIVEN, + aggregation: Sequence[TransactionRuleAggregationParam] | NotGiven = NOT_GIVEN, + evaluation_mode: Literal["ALL", "ANY"] | str | NotGiven = NOT_GIVEN, + scope: TransactionRuleScopeParam | NotGiven = NOT_GIVEN, + period_days: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleBacktestResult: + """Backtest rule configuration. + + Evaluate a hypothetical rule configuration against your recent transactions (most recent first, up to the backtest cap) without creating a rule or touching any transaction. Use it before activating a rule to see how often it would have matched and how many subjects it would have affected. + + ``POST /v3/transactions/rules/backtest/`` + + Args: + conditions + aggregation + evaluation_mode + scope: Which transactions the rule applies to. Only these three keys are accepted. + period_days + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleBacktestResult", + await self._client.request( + "post", + "/v3/transactions/rules/backtest/", + cast_to=TransactionRuleBacktestResult, + auth="api_key", + body={ + "conditions": conditions, + "aggregation": aggregation, + "evaluation_mode": evaluation_mode, + "scope": scope, + "period_days": period_days, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_library( + self, + *, + bundle: str | NotGiven = NOT_GIVEN, + category: str | NotGiven = NOT_GIVEN, + search: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[TransactionRuleLibraryEntry]: + """List rule library. + + Browse the full preset rule library (150+ rules) with each entry's complete configuration and an `is_installed` flag for this application. Filter by bundle, category, or free-text search. + + ``GET /v3/transactions/rules/library/`` + + Args: + bundle: Filter by bundle, e.g. finance, aml_ctf, crypto_monitoring, travel_rule + category: Filter by category + search: Case-insensitive match against title and description + limit: Page size + offset: Page offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[TransactionRuleLibraryEntry]", + self._client.request_page( + "get", + "/v3/transactions/rules/library/", + cast_to=TransactionsRulesListLibraryResponse, + item_type=TransactionRuleLibraryEntry, + auth="api_key", + query={ + "bundle": bundle, + "category": category, + "search": search, + "limit": limit, + "offset": offset, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def install_library( + self, + *, + library_keys: Sequence[str] | NotGiven = NOT_GIVEN, + bundle: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleInstallResult: + """Install library rules. + + Install one or more presets onto the application, by `library_keys` or a whole `bundle`. Installed presets evaluate immediately in their default mode; already-installed keys are skipped. + + ``POST /v3/transactions/rules/install/`` + + Args: + library_keys: Specific presets to install or uninstall. + bundle: Install or uninstall every preset in this bundle instead. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleInstallResult", + await self._client.request( + "post", + "/v3/transactions/rules/install/", + cast_to=TransactionRuleInstallResult, + auth="api_key", + body={"library_keys": library_keys, "bundle": bundle}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def uninstall_library( + self, + *, + library_keys: Sequence[str] | NotGiven = NOT_GIVEN, + bundle: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TransactionRuleInstallResult: + """Uninstall library rules. + + Remove installed presets from the application, by `library_keys` or a whole `bundle`. Uninstalled presets stop evaluating immediately and can be reinstalled from the library at any time. + + ``DELETE /v3/transactions/rules/install/`` + + Args: + library_keys: Specific presets to install or uninstall. + bundle: Install or uninstall every preset in this bundle instead. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TransactionRuleInstallResult", + await self._client.request( + "delete", + "/v3/transactions/rules/install/", + cast_to=TransactionRuleInstallResult, + auth="api_key", + body={"library_keys": library_keys, "bundle": bundle}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TransactionsRulesWithRawResponse: + def __init__(self, resource: TransactionsRules) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + self.backtest = to_raw_response_wrapper(resource.backtest) + self.list_library = to_raw_response_wrapper(resource.list_library) + self.install_library = to_raw_response_wrapper(resource.install_library) + self.uninstall_library = to_raw_response_wrapper(resource.uninstall_library) + + +class AsyncTransactionsRulesWithRawResponse: + def __init__(self, resource: AsyncTransactionsRules) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + self.backtest = async_to_raw_response_wrapper(resource.backtest) + self.list_library = async_to_raw_response_wrapper(resource.list_library) + self.install_library = async_to_raw_response_wrapper(resource.install_library) + self.uninstall_library = async_to_raw_response_wrapper(resource.uninstall_library) + + +class TransactionsRulesWithStreamingResponse: + def __init__(self, resource: TransactionsRules) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + self.backtest = to_streamed_response_wrapper(resource.backtest) + self.list_library = to_streamed_response_wrapper(resource.list_library) + self.install_library = to_streamed_response_wrapper(resource.install_library) + self.uninstall_library = to_streamed_response_wrapper(resource.uninstall_library) + + +class AsyncTransactionsRulesWithStreamingResponse: + def __init__(self, resource: AsyncTransactionsRules) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) + self.backtest = async_to_streamed_response_wrapper(resource.backtest) + self.list_library = async_to_streamed_response_wrapper(resource.list_library) + self.install_library = async_to_streamed_response_wrapper(resource.install_library) + self.uninstall_library = async_to_streamed_response_wrapper(resource.uninstall_library) diff --git a/src/didit/resources/transactions/travel_rule.py b/src/didit/resources/transactions/travel_rule.py new file mode 100644 index 0000000..df15cba --- /dev/null +++ b/src/didit/resources/transactions/travel_rule.py @@ -0,0 +1,254 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``transactions.travel_rule`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.travel_rule_transfer_detail import TravelRuleTransferDetail + +__all__ = [ + "TransactionsTravelRule", + "AsyncTransactionsTravelRule", + "TransactionsTravelRuleWithRawResponse", + "AsyncTransactionsTravelRuleWithRawResponse", + "TransactionsTravelRuleWithStreamingResponse", + "AsyncTransactionsTravelRuleWithStreamingResponse", +] + + +class TransactionsTravelRule(SyncAPIResource): + """The ``transactions.travel_rule`` resource of the Didit API. + + Methods: ``confirm_ownership``, ``update_transfer``. + """ + + @cached_property + def with_raw_response(self) -> TransactionsTravelRuleWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TransactionsTravelRuleWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TransactionsTravelRuleWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TransactionsTravelRuleWithStreamingResponse(self) + + def update_transfer( + self, + transaction_id: str, + *, + payment_txn_id: str | NotGiven = NOT_GIVEN, + action: Literal["cancel", "resend"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleTransferDetail: + """Finish, cancel, or resend a Travel Rule transfer. + + Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to re-run routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). + + ``PATCH /v3/transactions/{transaction_id}/travel-rule/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer. + payment_txn_id: The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status. + action: "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleTransferDetail", + self._client.request( + "patch", + "/v3/transactions/{transaction_id}/travel-rule/", + cast_to=TravelRuleTransferDetail, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={"payment_txn_id": payment_txn_id, "action": action}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def confirm_ownership( + self, + transaction_id: str, + *, + confirmed: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleTransferDetail: + """Confirm or deny wallet ownership. + + Called by the beneficiary's application when its INTERNAL-rail transfer is UNCONFIRMED_OWNERSHIP. Confirming marks the matching wallet address book entry as ownership-verified and runs the beneficiary name match on both sides of the transfer; denying declines both sides. + + ``POST /v3/transactions/{transaction_id}/travel-rule/ownership/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer (the beneficiary side's transaction). + confirmed + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleTransferDetail", + self._client.request( + "post", + "/v3/transactions/{transaction_id}/travel-rule/ownership/", + cast_to=TravelRuleTransferDetail, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={"confirmed": confirmed}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTransactionsTravelRule(AsyncAPIResource): + """The ``transactions.travel_rule`` resource of the Didit API. + + Methods: ``confirm_ownership``, ``update_transfer``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTransactionsTravelRuleWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTransactionsTravelRuleWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTransactionsTravelRuleWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTransactionsTravelRuleWithStreamingResponse(self) + + async def update_transfer( + self, + transaction_id: str, + *, + payment_txn_id: str | NotGiven = NOT_GIVEN, + action: Literal["cancel", "resend"] | str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleTransferDetail: + """Finish, cancel, or resend a Travel Rule transfer. + + Provide `payment_txn_id` to report the on-chain hash and move an outbound COMPLETED transfer to FINISHED (its INTERNAL-rail counterpart also moves to FINISHED), `{"action": "cancel"}` to cancel a non-terminal transfer, or `{"action": "resend"}` to re-run routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA (improve the counterparty data first, e.g. add a travel_address to the destination wallet entry). + + ``PATCH /v3/transactions/{transaction_id}/travel-rule/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer. + payment_txn_id: The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status. + action: "cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleTransferDetail", + await self._client.request( + "patch", + "/v3/transactions/{transaction_id}/travel-rule/", + cast_to=TravelRuleTransferDetail, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={"payment_txn_id": payment_txn_id, "action": action}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def confirm_ownership( + self, + transaction_id: str, + *, + confirmed: bool, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleTransferDetail: + """Confirm or deny wallet ownership. + + Called by the beneficiary's application when its INTERNAL-rail transfer is UNCONFIRMED_OWNERSHIP. Confirming marks the matching wallet address book entry as ownership-verified and runs the beneficiary name match on both sides of the transfer; denying declines both sides. + + ``POST /v3/transactions/{transaction_id}/travel-rule/ownership/`` + + Args: + transaction_id: Didit-stable transaction UUID that owns the transfer (the beneficiary side's transaction). + confirmed + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleTransferDetail", + await self._client.request( + "post", + "/v3/transactions/{transaction_id}/travel-rule/ownership/", + cast_to=TravelRuleTransferDetail, + path_params={"transaction_id": transaction_id}, + auth="api_key", + body={"confirmed": confirmed}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TransactionsTravelRuleWithRawResponse: + def __init__(self, resource: TransactionsTravelRule) -> None: + self._resource = resource + self.update_transfer = to_raw_response_wrapper(resource.update_transfer) + self.confirm_ownership = to_raw_response_wrapper(resource.confirm_ownership) + + +class AsyncTransactionsTravelRuleWithRawResponse: + def __init__(self, resource: AsyncTransactionsTravelRule) -> None: + self._resource = resource + self.update_transfer = async_to_raw_response_wrapper(resource.update_transfer) + self.confirm_ownership = async_to_raw_response_wrapper(resource.confirm_ownership) + + +class TransactionsTravelRuleWithStreamingResponse: + def __init__(self, resource: TransactionsTravelRule) -> None: + self._resource = resource + self.update_transfer = to_streamed_response_wrapper(resource.update_transfer) + self.confirm_ownership = to_streamed_response_wrapper(resource.confirm_ownership) + + +class AsyncTransactionsTravelRuleWithStreamingResponse: + def __init__(self, resource: AsyncTransactionsTravelRule) -> None: + self._resource = resource + self.update_transfer = async_to_streamed_response_wrapper(resource.update_transfer) + self.confirm_ownership = async_to_streamed_response_wrapper(resource.confirm_ownership) diff --git a/src/didit/resources/travel_rule/__init__.py b/src/didit/resources/travel_rule/__init__.py new file mode 100644 index 0000000..4d844e8 --- /dev/null +++ b/src/didit/resources/travel_rule/__init__.py @@ -0,0 +1,738 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``travel_rule`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING, Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.travel_rule_inbound_response import TravelRuleInboundResponse +from didit.types.components.travel_rule_settings_detail import TravelRuleSettingsDetail +from didit.types.components.travel_rule_widget_session_response import ( + TravelRuleWidgetSessionResponse, +) +from didit.types.components.vasp_directory_entry import VaspDirectoryEntry +from didit.types.travel_rule.list_vasps_response import TravelRuleListVaspsResponse +from didit.types.travel_rule.register_inbound_params import ( + TravelRuleRegisterInboundParamsOriginatingVasp, +) + +if TYPE_CHECKING: + from didit.resources.travel_rule.wallet_addresses import ( + AsyncTravelRuleWalletAddresses, + AsyncTravelRuleWalletAddressesWithRawResponse, + AsyncTravelRuleWalletAddressesWithStreamingResponse, + TravelRuleWalletAddresses, + TravelRuleWalletAddressesWithRawResponse, + TravelRuleWalletAddressesWithStreamingResponse, + ) + +__all__ = [ + "TravelRule", + "AsyncTravelRule", + "TravelRuleWithRawResponse", + "AsyncTravelRuleWithRawResponse", + "TravelRuleWithStreamingResponse", + "AsyncTravelRuleWithStreamingResponse", +] + + +class TravelRule(SyncAPIResource): + """The ``travel_rule`` resource of the Didit API. + + Methods: ``create_widget_session``, ``list_vasps``, ``register_inbound``, ``retrieve_settings``, ``update_settings``. + + Nested resources: ``wallet_addresses``. + """ + + @cached_property + def wallet_addresses(self) -> TravelRuleWalletAddresses: + from didit.resources.travel_rule.wallet_addresses import TravelRuleWalletAddresses + + return TravelRuleWalletAddresses(self._client) + + @cached_property + def with_raw_response(self) -> TravelRuleWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TravelRuleWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TravelRuleWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TravelRuleWithStreamingResponse(self) + + def retrieve_settings( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSettingsDetail: + """Get Travel Rule settings. + + Read the application's Travel Rule settings and VASP profile. Auto-creates a default (disabled) settings row and an empty profile on first read. + + ``GET /v3/travel-rule/settings/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSettingsDetail", + self._client.request( + "get", + "/v3/travel-rule/settings/", + cast_to=TravelRuleSettingsDetail, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_settings( + self, + *, + is_enabled: bool | NotGiven = NOT_GIVEN, + legal_name: str | NotGiven = NOT_GIVEN, + lei: str | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] + | str + | NotGiven = NOT_GIVEN, + confirmation_timeout_hours: int | NotGiven = NOT_GIVEN, + timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str | NotGiven = NOT_GIVEN, + compliance_email: str | NotGiven = NOT_GIVEN, + is_discoverable: bool | NotGiven = NOT_GIVEN, + threshold_amount: str | NotGiven = NOT_GIVEN, + inbound_auto_accept: bool | NotGiven = NOT_GIVEN, + allow_self_declaration: bool | NotGiven = NOT_GIVEN, + allow_screenshot_proof: bool | NotGiven = NOT_GIVEN, + auto_wallet_verification: bool | NotGiven = NOT_GIVEN, + vasp_attribution_enabled: bool | NotGiven = NOT_GIVEN, + disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSettingsDetail: + """Update Travel Rule settings. + + Upsert the application's Travel Rule settings and VASP profile. Partial update - only send the fields you want to change. Setting `is_enabled: true` requires a non-blank `legal_name` (already stored or included in the same request), otherwise returns 400. + + ``PUT /v3/travel-rule/settings/`` + + Args: + is_enabled + legal_name + lei + jurisdiction + name_matching_strictness + confirmation_timeout_hours + timeout_outcome + compliance_email + is_discoverable + threshold_amount: Decimal string. Minimum transfer amount that triggers a managed exchange. + inbound_auto_accept + allow_self_declaration + allow_screenshot_proof: When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true. + auto_wallet_verification + vasp_attribution_enabled + disabled_networks: Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSettingsDetail", + self._client.request( + "put", + "/v3/travel-rule/settings/", + cast_to=TravelRuleSettingsDetail, + auth="api_key", + body={ + "is_enabled": is_enabled, + "legal_name": legal_name, + "lei": lei, + "jurisdiction": jurisdiction, + "name_matching_strictness": name_matching_strictness, + "confirmation_timeout_hours": confirmation_timeout_hours, + "timeout_outcome": timeout_outcome, + "compliance_email": compliance_email, + "is_discoverable": is_discoverable, + "threshold_amount": threshold_amount, + "inbound_auto_accept": inbound_auto_accept, + "allow_self_declaration": allow_self_declaration, + "allow_screenshot_proof": allow_screenshot_proof, + "auto_wallet_verification": auto_wallet_verification, + "vasp_attribution_enabled": vasp_attribution_enabled, + "disabled_networks": disabled_networks, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_vasps( + self, + *, + search: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[VaspDirectoryEntry]: + """Search the VASP directory. + + Paginated search across discoverable Didit customers with Travel Rule enabled, plus VASPs catalogued in the internal counterparty registry. Excludes your own application's profile. + + ``GET /v3/travel-rule/vasps/`` + + Args: + search: Case-insensitive substring match on VASP name. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[VaspDirectoryEntry]", + self._client.request_page( + "get", + "/v3/travel-rule/vasps/", + cast_to=TravelRuleListVaspsResponse, + item_type=VaspDirectoryEntry, + auth="api_key", + query={"search": search, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def register_inbound( + self, + *, + chain: str, + txid: str, + wallet_address: str, + amount: str, + currency: str, + originator_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + beneficiary_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleInboundResponse: + """Register an after-deposit (sunrise) inbound transfer. + + Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. A transaction.created webhook fires for newly-created transactions. + + ``POST /v3/travel-rule/inbound/`` + + Args: + chain: Chain the deposit settled on, e.g. "ethereum". + txid: On-chain transaction hash. Deduped together with wallet_address. + wallet_address: The destination wallet you received the deposit on. + amount: Deposit amount as a decimal string. + currency: Deposit asset, e.g. "USDC". + originator_data: IVMS-101 originator payload from the sending VASP, if known. + beneficiary_data: IVMS-101 beneficiary payload (your customer). + originating_vasp: Optional description of the sending VASP. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleInboundResponse", + self._client.request( + "post", + "/v3/travel-rule/inbound/", + cast_to=TravelRuleInboundResponse, + auth="api_key", + body={ + "chain": chain, + "txid": txid, + "wallet_address": wallet_address, + "amount": amount, + "currency": currency, + "originator_data": originator_data, + "beneficiary_data": beneficiary_data, + "originating_vasp": originating_vasp, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create_widget_session( + self, + *, + wallet_address: str, + chain: str, + holder_name: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + transaction_id: str | NotGiven = NOT_GIVEN, + satoshi_deposit_address: str | NotGiven = NOT_GIVEN, + callback_url: str | NotGiven = NOT_GIVEN, + expires_in_minutes: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWidgetSessionResponse: + """Mint a wallet-ownership widget session. + + Mint a hosted wallet-ownership widget URL/token. Send the customer to the returned url to prove control of a wallet (message signing, Satoshi test, screenshot, or self-declaration). The widget is web-only in this release - open the url in the system browser or a custom tab, never inside a mobile SDK webview. + + ``POST /v3/travel-rule/widget-session/`` + + Args: + wallet_address: The wallet address to verify. + chain: Chain identifier for the address, e.g. "ethereum", "bitcoin", "solana". + holder_name: Name of the wallet holder, used for the beneficiary name match. + vendor_data: Your internal identifier for the holder, carried onto the created address-book entry. + transaction_id: Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies. + satoshi_deposit_address: Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method. + callback_url: Optional. Where the widget returns the customer after completion. + expires_in_minutes: Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWidgetSessionResponse", + self._client.request( + "post", + "/v3/travel-rule/widget-session/", + cast_to=TravelRuleWidgetSessionResponse, + auth="api_key", + body={ + "wallet_address": wallet_address, + "chain": chain, + "holder_name": holder_name, + "vendor_data": vendor_data, + "transaction_id": transaction_id, + "satoshi_deposit_address": satoshi_deposit_address, + "callback_url": callback_url, + "expires_in_minutes": expires_in_minutes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTravelRule(AsyncAPIResource): + """The ``travel_rule`` resource of the Didit API. + + Methods: ``create_widget_session``, ``list_vasps``, ``register_inbound``, ``retrieve_settings``, ``update_settings``. + + Nested resources: ``wallet_addresses``. + """ + + @cached_property + def wallet_addresses(self) -> AsyncTravelRuleWalletAddresses: + from didit.resources.travel_rule.wallet_addresses import AsyncTravelRuleWalletAddresses + + return AsyncTravelRuleWalletAddresses(self._client) + + @cached_property + def with_raw_response(self) -> AsyncTravelRuleWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTravelRuleWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTravelRuleWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTravelRuleWithStreamingResponse(self) + + async def retrieve_settings( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSettingsDetail: + """Get Travel Rule settings. + + Read the application's Travel Rule settings and VASP profile. Auto-creates a default (disabled) settings row and an empty profile on first read. + + ``GET /v3/travel-rule/settings/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSettingsDetail", + await self._client.request( + "get", + "/v3/travel-rule/settings/", + cast_to=TravelRuleSettingsDetail, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_settings( + self, + *, + is_enabled: bool | NotGiven = NOT_GIVEN, + legal_name: str | NotGiven = NOT_GIVEN, + lei: str | NotGiven = NOT_GIVEN, + jurisdiction: str | NotGiven = NOT_GIVEN, + name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] + | str + | NotGiven = NOT_GIVEN, + confirmation_timeout_hours: int | NotGiven = NOT_GIVEN, + timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str | NotGiven = NOT_GIVEN, + compliance_email: str | NotGiven = NOT_GIVEN, + is_discoverable: bool | NotGiven = NOT_GIVEN, + threshold_amount: str | NotGiven = NOT_GIVEN, + inbound_auto_accept: bool | NotGiven = NOT_GIVEN, + allow_self_declaration: bool | NotGiven = NOT_GIVEN, + allow_screenshot_proof: bool | NotGiven = NOT_GIVEN, + auto_wallet_verification: bool | NotGiven = NOT_GIVEN, + vasp_attribution_enabled: bool | NotGiven = NOT_GIVEN, + disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleSettingsDetail: + """Update Travel Rule settings. + + Upsert the application's Travel Rule settings and VASP profile. Partial update - only send the fields you want to change. Setting `is_enabled: true` requires a non-blank `legal_name` (already stored or included in the same request), otherwise returns 400. + + ``PUT /v3/travel-rule/settings/`` + + Args: + is_enabled + legal_name + lei + jurisdiction + name_matching_strictness + confirmation_timeout_hours + timeout_outcome + compliance_email + is_discoverable + threshold_amount: Decimal string. Minimum transfer amount that triggers a managed exchange. + inbound_auto_accept + allow_self_declaration + allow_screenshot_proof: When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true. + auto_wallet_verification + vasp_attribution_enabled + disabled_networks: Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleSettingsDetail", + await self._client.request( + "put", + "/v3/travel-rule/settings/", + cast_to=TravelRuleSettingsDetail, + auth="api_key", + body={ + "is_enabled": is_enabled, + "legal_name": legal_name, + "lei": lei, + "jurisdiction": jurisdiction, + "name_matching_strictness": name_matching_strictness, + "confirmation_timeout_hours": confirmation_timeout_hours, + "timeout_outcome": timeout_outcome, + "compliance_email": compliance_email, + "is_discoverable": is_discoverable, + "threshold_amount": threshold_amount, + "inbound_auto_accept": inbound_auto_accept, + "allow_self_declaration": allow_self_declaration, + "allow_screenshot_proof": allow_screenshot_proof, + "auto_wallet_verification": auto_wallet_verification, + "vasp_attribution_enabled": vasp_attribution_enabled, + "disabled_networks": disabled_networks, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_vasps( + self, + *, + search: str | NotGiven = NOT_GIVEN, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[VaspDirectoryEntry]: + """Search the VASP directory. + + Paginated search across discoverable Didit customers with Travel Rule enabled, plus VASPs catalogued in the internal counterparty registry. Excludes your own application's profile. + + ``GET /v3/travel-rule/vasps/`` + + Args: + search: Case-insensitive substring match on VASP name. + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[VaspDirectoryEntry]", + self._client.request_page( + "get", + "/v3/travel-rule/vasps/", + cast_to=TravelRuleListVaspsResponse, + item_type=VaspDirectoryEntry, + auth="api_key", + query={"search": search, "limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def register_inbound( + self, + *, + chain: str, + txid: str, + wallet_address: str, + amount: str, + currency: str, + originator_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + beneficiary_data: Mapping[str, object] | NotGiven = NOT_GIVEN, + originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleInboundResponse: + """Register an after-deposit (sunrise) inbound transfer. + + Register Travel Rule data for a crypto deposit that already settled on-chain before any exchange took place. Creates an inbound travelRule transaction (or reuses an unclaimed deposit) and resolves the transfer against your wallet address book. A transaction.created webhook fires for newly-created transactions. + + ``POST /v3/travel-rule/inbound/`` + + Args: + chain: Chain the deposit settled on, e.g. "ethereum". + txid: On-chain transaction hash. Deduped together with wallet_address. + wallet_address: The destination wallet you received the deposit on. + amount: Deposit amount as a decimal string. + currency: Deposit asset, e.g. "USDC". + originator_data: IVMS-101 originator payload from the sending VASP, if known. + beneficiary_data: IVMS-101 beneficiary payload (your customer). + originating_vasp: Optional description of the sending VASP. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleInboundResponse", + await self._client.request( + "post", + "/v3/travel-rule/inbound/", + cast_to=TravelRuleInboundResponse, + auth="api_key", + body={ + "chain": chain, + "txid": txid, + "wallet_address": wallet_address, + "amount": amount, + "currency": currency, + "originator_data": originator_data, + "beneficiary_data": beneficiary_data, + "originating_vasp": originating_vasp, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create_widget_session( + self, + *, + wallet_address: str, + chain: str, + holder_name: str | NotGiven = NOT_GIVEN, + vendor_data: str | NotGiven = NOT_GIVEN, + transaction_id: str | NotGiven = NOT_GIVEN, + satoshi_deposit_address: str | NotGiven = NOT_GIVEN, + callback_url: str | NotGiven = NOT_GIVEN, + expires_in_minutes: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> TravelRuleWidgetSessionResponse: + """Mint a wallet-ownership widget session. + + Mint a hosted wallet-ownership widget URL/token. Send the customer to the returned url to prove control of a wallet (message signing, Satoshi test, screenshot, or self-declaration). The widget is web-only in this release - open the url in the system browser or a custom tab, never inside a mobile SDK webview. + + ``POST /v3/travel-rule/widget-session/`` + + Args: + wallet_address: The wallet address to verify. + chain: Chain identifier for the address, e.g. "ethereum", "bitcoin", "solana". + holder_name: Name of the wallet holder, used for the beneficiary name match. + vendor_data: Your internal identifier for the holder, carried onto the created address-book entry. + transaction_id: Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies. + satoshi_deposit_address: Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method. + callback_url: Optional. Where the widget returns the customer after completion. + expires_in_minutes: Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "TravelRuleWidgetSessionResponse", + await self._client.request( + "post", + "/v3/travel-rule/widget-session/", + cast_to=TravelRuleWidgetSessionResponse, + auth="api_key", + body={ + "wallet_address": wallet_address, + "chain": chain, + "holder_name": holder_name, + "vendor_data": vendor_data, + "transaction_id": transaction_id, + "satoshi_deposit_address": satoshi_deposit_address, + "callback_url": callback_url, + "expires_in_minutes": expires_in_minutes, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TravelRuleWithRawResponse: + def __init__(self, resource: TravelRule) -> None: + self._resource = resource + self.retrieve_settings = to_raw_response_wrapper(resource.retrieve_settings) + self.update_settings = to_raw_response_wrapper(resource.update_settings) + self.list_vasps = to_raw_response_wrapper(resource.list_vasps) + self.register_inbound = to_raw_response_wrapper(resource.register_inbound) + self.create_widget_session = to_raw_response_wrapper(resource.create_widget_session) + + @cached_property + def wallet_addresses(self) -> TravelRuleWalletAddressesWithRawResponse: + from didit.resources.travel_rule.wallet_addresses import ( + TravelRuleWalletAddressesWithRawResponse, + ) + + return TravelRuleWalletAddressesWithRawResponse(self._resource.wallet_addresses) + + +class AsyncTravelRuleWithRawResponse: + def __init__(self, resource: AsyncTravelRule) -> None: + self._resource = resource + self.retrieve_settings = async_to_raw_response_wrapper(resource.retrieve_settings) + self.update_settings = async_to_raw_response_wrapper(resource.update_settings) + self.list_vasps = async_to_raw_response_wrapper(resource.list_vasps) + self.register_inbound = async_to_raw_response_wrapper(resource.register_inbound) + self.create_widget_session = async_to_raw_response_wrapper(resource.create_widget_session) + + @cached_property + def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithRawResponse: + from didit.resources.travel_rule.wallet_addresses import ( + AsyncTravelRuleWalletAddressesWithRawResponse, + ) + + return AsyncTravelRuleWalletAddressesWithRawResponse(self._resource.wallet_addresses) + + +class TravelRuleWithStreamingResponse: + def __init__(self, resource: TravelRule) -> None: + self._resource = resource + self.retrieve_settings = to_streamed_response_wrapper(resource.retrieve_settings) + self.update_settings = to_streamed_response_wrapper(resource.update_settings) + self.list_vasps = to_streamed_response_wrapper(resource.list_vasps) + self.register_inbound = to_streamed_response_wrapper(resource.register_inbound) + self.create_widget_session = to_streamed_response_wrapper(resource.create_widget_session) + + @cached_property + def wallet_addresses(self) -> TravelRuleWalletAddressesWithStreamingResponse: + from didit.resources.travel_rule.wallet_addresses import ( + TravelRuleWalletAddressesWithStreamingResponse, + ) + + return TravelRuleWalletAddressesWithStreamingResponse(self._resource.wallet_addresses) + + +class AsyncTravelRuleWithStreamingResponse: + def __init__(self, resource: AsyncTravelRule) -> None: + self._resource = resource + self.retrieve_settings = async_to_streamed_response_wrapper(resource.retrieve_settings) + self.update_settings = async_to_streamed_response_wrapper(resource.update_settings) + self.list_vasps = async_to_streamed_response_wrapper(resource.list_vasps) + self.register_inbound = async_to_streamed_response_wrapper(resource.register_inbound) + self.create_widget_session = async_to_streamed_response_wrapper( + resource.create_widget_session + ) + + @cached_property + def wallet_addresses(self) -> AsyncTravelRuleWalletAddressesWithStreamingResponse: + from didit.resources.travel_rule.wallet_addresses import ( + AsyncTravelRuleWalletAddressesWithStreamingResponse, + ) + + return AsyncTravelRuleWalletAddressesWithStreamingResponse(self._resource.wallet_addresses) diff --git a/src/didit/resources/travel_rule/wallet_addresses.py b/src/didit/resources/travel_rule/wallet_addresses.py new file mode 100644 index 0000000..d988e6c --- /dev/null +++ b/src/didit/resources/travel_rule/wallet_addresses.py @@ -0,0 +1,473 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``travel_rule.wallet_addresses`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.wallet_address_entry import WalletAddressEntry +from didit.types.components.wallet_address_entry_update import WalletAddressEntryUpdate +from didit.types.travel_rule.wallet_addresses.list_response import ( + TravelRuleWalletAddressesListResponse, +) + +__all__ = [ + "TravelRuleWalletAddresses", + "AsyncTravelRuleWalletAddresses", + "TravelRuleWalletAddressesWithRawResponse", + "AsyncTravelRuleWalletAddressesWithRawResponse", + "TravelRuleWalletAddressesWithStreamingResponse", + "AsyncTravelRuleWalletAddressesWithStreamingResponse", +] + + +class TravelRuleWalletAddresses(SyncAPIResource): + """The ``travel_rule.wallet_addresses`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> TravelRuleWalletAddressesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return TravelRuleWalletAddressesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> TravelRuleWalletAddressesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return TravelRuleWalletAddressesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[WalletAddressEntry]: + """List wallet address book entries. + + Paginated list of your application's Travel Rule wallet address book, newest first. + + ``GET /v3/travel-rule/wallet-addresses/`` + + Args: + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[WalletAddressEntry]", + self._client.request_page( + "get", + "/v3/travel-rule/wallet-addresses/", + cast_to=TravelRuleWalletAddressesListResponse, + item_type=WalletAddressEntry, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + address: str, + chain: str | NotGiven = NOT_GIVEN, + holder_vendor_data: str | NotGiven = NOT_GIVEN, + holder_name: str | NotGiven = NOT_GIVEN, + entity_type: str | NotGiven = NOT_GIVEN, + self_declared: bool | NotGiven = NOT_GIVEN, + travel_address: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletAddressEntry: + """Register a wallet address. + + Add an entry to your Travel Rule wallet address book so inbound INTERNAL-rail transfers can resolve against it. + + ``POST /v3/travel-rule/wallet-addresses/`` + + Args: + address + chain + holder_vendor_data + holder_name + entity_type + self_declared: Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. + travel_address: Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletAddressEntry", + self._client.request( + "post", + "/v3/travel-rule/wallet-addresses/", + cast_to=WalletAddressEntry, + auth="api_key", + body={ + "address": address, + "chain": chain, + "holder_vendor_data": holder_vendor_data, + "holder_name": holder_name, + "entity_type": entity_type, + "self_declared": self_declared, + "travel_address": travel_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + entry_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a wallet address. + + Deletes the entry. It stops matching new transfers immediately and the address is freed up for re-registration. + + ``DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/`` + + Args: + entry_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/travel-rule/wallet-addresses/{entry_uuid}/", + cast_to=None, + path_params={"entry_uuid": entry_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + entry_uuid: str, + *, + holder_name: str | NotGiven = NOT_GIVEN, + holder_vendor_data: str | NotGiven = NOT_GIVEN, + entity_type: str | NotGiven = NOT_GIVEN, + travel_address: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletAddressEntryUpdate: + """Update wallet address holder metadata. + + Partial update of holder_name, holder_vendor_data, entity_type, and travel_address. Setting travel_address attaches a TRP routing address to the entry; invalid values are rejected with 400. The response echoes only these four fields. + + ``PATCH /v3/travel-rule/wallet-addresses/{entry_uuid}/`` + + Args: + entry_uuid + holder_name + holder_vendor_data + entity_type + travel_address + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletAddressEntryUpdate", + self._client.request( + "patch", + "/v3/travel-rule/wallet-addresses/{entry_uuid}/", + cast_to=WalletAddressEntryUpdate, + path_params={"entry_uuid": entry_uuid}, + auth="api_key", + body={ + "holder_name": holder_name, + "holder_vendor_data": holder_vendor_data, + "entity_type": entity_type, + "travel_address": travel_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncTravelRuleWalletAddresses(AsyncAPIResource): + """The ``travel_rule.wallet_addresses`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncTravelRuleWalletAddressesWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncTravelRuleWalletAddressesWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncTravelRuleWalletAddressesWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncTravelRuleWalletAddressesWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[WalletAddressEntry]: + """List wallet address book entries. + + Paginated list of your application's Travel Rule wallet address book, newest first. + + ``GET /v3/travel-rule/wallet-addresses/`` + + Args: + limit + offset + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[WalletAddressEntry]", + self._client.request_page( + "get", + "/v3/travel-rule/wallet-addresses/", + cast_to=TravelRuleWalletAddressesListResponse, + item_type=WalletAddressEntry, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + address: str, + chain: str | NotGiven = NOT_GIVEN, + holder_vendor_data: str | NotGiven = NOT_GIVEN, + holder_name: str | NotGiven = NOT_GIVEN, + entity_type: str | NotGiven = NOT_GIVEN, + self_declared: bool | NotGiven = NOT_GIVEN, + travel_address: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletAddressEntry: + """Register a wallet address. + + Add an entry to your Travel Rule wallet address book so inbound INTERNAL-rail transfers can resolve against it. + + ``POST /v3/travel-rule/wallet-addresses/`` + + Args: + address + chain + holder_vendor_data + holder_name + entity_type + self_declared: Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof. + travel_address: Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletAddressEntry", + await self._client.request( + "post", + "/v3/travel-rule/wallet-addresses/", + cast_to=WalletAddressEntry, + auth="api_key", + body={ + "address": address, + "chain": chain, + "holder_vendor_data": holder_vendor_data, + "holder_name": holder_name, + "entity_type": entity_type, + "self_declared": self_declared, + "travel_address": travel_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + entry_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete a wallet address. + + Deletes the entry. It stops matching new transfers immediately and the address is freed up for re-registration. + + ``DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/`` + + Args: + entry_uuid + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/travel-rule/wallet-addresses/{entry_uuid}/", + cast_to=None, + path_params={"entry_uuid": entry_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + entry_uuid: str, + *, + holder_name: str | NotGiven = NOT_GIVEN, + holder_vendor_data: str | NotGiven = NOT_GIVEN, + entity_type: str | NotGiven = NOT_GIVEN, + travel_address: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletAddressEntryUpdate: + """Update wallet address holder metadata. + + Partial update of holder_name, holder_vendor_data, entity_type, and travel_address. Setting travel_address attaches a TRP routing address to the entry; invalid values are rejected with 400. The response echoes only these four fields. + + ``PATCH /v3/travel-rule/wallet-addresses/{entry_uuid}/`` + + Args: + entry_uuid + holder_name + holder_vendor_data + entity_type + travel_address + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletAddressEntryUpdate", + await self._client.request( + "patch", + "/v3/travel-rule/wallet-addresses/{entry_uuid}/", + cast_to=WalletAddressEntryUpdate, + path_params={"entry_uuid": entry_uuid}, + auth="api_key", + body={ + "holder_name": holder_name, + "holder_vendor_data": holder_vendor_data, + "entity_type": entity_type, + "travel_address": travel_address, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class TravelRuleWalletAddressesWithRawResponse: + def __init__(self, resource: TravelRuleWalletAddresses) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncTravelRuleWalletAddressesWithRawResponse: + def __init__(self, resource: AsyncTravelRuleWalletAddresses) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + + +class TravelRuleWalletAddressesWithStreamingResponse: + def __init__(self, resource: TravelRuleWalletAddresses) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncTravelRuleWalletAddressesWithStreamingResponse: + def __init__(self, resource: AsyncTravelRuleWalletAddresses) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/users.py b/src/didit/resources/users.py new file mode 100644 index 0000000..b10d881 --- /dev/null +++ b/src/didit/resources/users.py @@ -0,0 +1,1346 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``users`` resource.""" + +from __future__ import annotations + +import datetime as _dt +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.user_detail_item import UserDetailItem +from didit.types.components.user_list_item import UserListItem +from didit.types.users.batch_delete_response import UsersBatchDeleteResponse +from didit.types.users.list_networks_response import UsersListNetworksResponse +from didit.types.users.list_response import UsersListResponse +from didit.types.users.retrieve_ongoing_monitoring_job_response import ( + UsersRetrieveOngoingMonitoringJobResponse, +) +from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponse202, + UsersSetAmlMonitoringResponseVariant1, + UsersSetAmlMonitoringResponseVariant2, +) +from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponse202, + UsersSetOngoingMonitoringResponseVariant1, + UsersSetOngoingMonitoringResponseVariant2, +) +from didit.types.users.upload_face_response import UsersUploadFaceResponse + +__all__ = [ + "Users", + "AsyncUsers", + "UsersWithRawResponse", + "AsyncUsersWithRawResponse", + "UsersWithStreamingResponse", + "AsyncUsersWithStreamingResponse", +] + + +class Users(SyncAPIResource): + """The ``users`` resource of the Didit API. + + Methods: ``batch_delete``, ``create``, ``generate_pdf``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``update``, ``update_status``, ``upload_face``. + """ + + @cached_property + def with_raw_response(self) -> UsersWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return UsersWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> UsersWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return UsersWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[UserListItem]: + """List users. + + List [users](/entities/users) Didit assembles from KYC sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus users created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters - fetch pages and filter client-side. + + ``GET /v3/users/`` + + Args: + limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. + offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[UserListItem]", + self._client.request_page( + "get", + "/v3/users/", + cast_to=UsersListResponse, + item_type=UserListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + vendor_data: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Get user. + + Fetch one [user](/entities/users) by `vendor_data` (exact match). Adds `metadata`, `comments`, and `updated_at` to the list view, and switches `tags` to the detailed tag-link shape. + + ``GET /v3/users/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the user - a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the session that first introduced this user, matched exactly as sent. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + self._client.request( + "get", + "/v3/users/{vendor_data}/", + cast_to=UserDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + vendor_data: str, + *, + full_name: str | None | NotGiven = NOT_GIVEN, + display_name: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, + approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, + issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Update user. + + Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. **Note:** unlike [Update User Status](#patch-/v3/users/-vendor_data-/update-status/), changing `status` here does NOT record a status-change activity and does NOT sync the system blocklist - use the dedicated update-status endpoint for `BLOCKED`/unblock flows. + + ``PATCH /v3/users/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the user (free-form string, NOT a UUID). + full_name: Full name of the user + display_name: Custom display name for this user + date_of_birth: Date of birth in YYYY-MM-DD format + status: User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint. + metadata: Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. + approved_emails: Verified email addresses for this user, e.g. `["john@example.com"]`. Fully replaces the stored list. + approved_phones: Verified phone numbers for this user. Fully replaces the stored list. + issuing_states: Issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. Fully replaces the stored list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + self._client.request( + "patch", + "/v3/users/{vendor_data}/", + cast_to=UserDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={ + "full_name": full_name, + "display_name": display_name, + "date_of_birth": date_of_birth, + "status": status, + "metadata": metadata, + "approved_emails": approved_emails, + "approved_phones": approved_phones, + "issuing_states": issuing_states, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def generate_pdf( + self, + vendor_data: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download the full user history PDF report. + + Render and download one compliance-ready PDF containing the full verification history of the user identified by `vendor_data`. It is the same document the Console produces from **User detail → Actions → Download PDF**, so periodic compliance exports no longer need a person clicking in the Console. + + **What the report contains.** A cover page with the profile summary (name, date of birth, issuing states, approved emails and phones), the latest status of every feature, the session counters and an index of the sessions included - followed by the standard report of every included session, in chronological order (oldest first). Each session report is the same document `GET /v3/session/{sessionId}/generate-pdf/` returns for that session. + + **Eligible sessions.** Only User Verification (KYC) sessions in `Approved`, `Declined`, `In Review` or `Kyc Expired` are included; sessions in any other status are skipped. If the user has no session in one of those statuses the endpoint returns `403` - see below. + + **Cap.** The report is capped at the **20 most recent** reportable sessions. When a user has more, the newest 20 are included and the cover page states how many older sessions were left out. Use `GET /v3/sessions/?vendor_data=…` for the complete list as JSON. + + **Branding.** If your application has white-label customization, the cover and every session report are rendered with your logo and privacy-policy URL; otherwise they carry Didit branding. Reports are rendered in English - there is no language parameter. + + **No caching.** Every call re-renders the PDF from the current data, so a report generated after a manual review reflects the reviewer's decision. Two calls for the same user can produce byte-different files - archive the downloaded file if you need an immutable copy. + + **Latency.** Rendering is synchronous and downloads every stored image of every included session, so a user with many media-heavy sessions can take tens of seconds. Use a generous client read timeout (120 s recommended) and stream the body to disk. Generation stops at a server-side time budget instead of running until your client gives up: if that budget is reached, the response is still a valid PDF containing the most recent sessions, and the cover states how many were omitted. + + **Scope.** The user is resolved inside the application that owns the API key. A key can never export a user of another application - such a user does not exist for it and the endpoint returns `404`. + + **Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response - there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples. + + ``GET /v3/users/{vendor_data}/generate-pdf/`` + + Args: + vendor_data: Your unique identifier for the user - a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the user's sessions, matched exactly as sent. URL-encode it; a value containing `/` cannot be addressed by this route. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "get", + "/v3/users/{vendor_data}/generate-pdf/", + cast_to=bytes, + path_params={"vendor_data": vendor_data}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def batch_delete( + self, + *, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersBatchDeleteResponse: + """Batch delete users. + + **Permanently delete** users - the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. The Console's `filter_*` batch params are rejected with a `400` rather than ignored, so a narrowed intent can never turn into a full wipe. + + **Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job. + + ``POST /v3/users/delete/`` + + Args: + vendor_data_list: Your own identifiers to delete, matched exactly as sent. + didit_internal_id_list: Didit internal user IDs to delete - the `didit_internal_id` returned by the create and get endpoints. + delete_all: If `true`, deletes every user in the application. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersBatchDeleteResponse", + self._client.request( + "post", + "/v3/users/delete/", + cast_to=UsersBatchDeleteResponse, + auth="api_key", + body={ + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + "delete_all": delete_all, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def set_aml_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + UsersSetAmlMonitoringResponseVariant1 + | UsersSetAmlMonitoringResponseVariant2 + | UsersSetAmlMonitoringResponse202 + ): + """Enable or disable AML ongoing monitoring for users. + + Turns AML ongoing monitoring on or off for the selected users, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per user, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Users without any AML screening are screened first (billed as a regular AML screening) when they have the full name, date of birth and country needed; otherwise they are reported as `skipped_no_identity_data`. + + Set `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints. + + ``POST /v3/users/aml-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes - no screening, no billing, no job. + vendor_data_list: Your own identifiers of the users to toggle, matched exactly as sent. + didit_internal_id_list: Didit internal ids of the users to toggle - the `didit_internal_id` returned by the create, get and list endpoints. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersSetAmlMonitoringResponseVariant1 | UsersSetAmlMonitoringResponseVariant2 | UsersSetAmlMonitoringResponse202", + self._client.request( + "post", + "/v3/users/aml-monitoring/", + cast_to={ + 200: UsersSetAmlMonitoringResponseVariant1 + | UsersSetAmlMonitoringResponseVariant2, + 202: UsersSetAmlMonitoringResponse202, + }, + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update_status( + self, + vendor_data: str, + *, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Update user status. + + Flip only the lifecycle `status` of a user - `ACTIVE`/`FLAGGED`/`BLOCKED` (NOT session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user. + + ``PATCH /v3/users/{vendor_data}/update-status/`` + + Args: + vendor_data: Your unique identifier for the user. + status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + self._client.request( + "patch", + "/v3/users/{vendor_data}/update-status/", + cast_to=UserDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={"status": status}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + vendor_data: str, + full_name: str | None | NotGiven = NOT_GIVEN, + display_name: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, + approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, + issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Create user. + + Pre-create a [user](/entities/users) by `vendor_data` *without* a verification session. `vendor_data` must be unique among non-deleted users for the application (exact match; conflicts return 400). Not idempotent. + + ``POST /v3/users/create/`` + + Args: + vendor_data: Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent. + full_name: Full legal name of the user. + display_name: Friendly display name shown in the console (takes precedence over `full_name` for UI display). + date_of_birth: Date of birth in `YYYY-MM-DD` format. + status: Initial lifecycle status. Defaults to `ACTIVE`. + metadata: Arbitrary JSON object you attach to the user. Defaults to `{}`. + approved_emails: Pre-trusted email addresses for this user, e.g. `["john@example.com"]`. + approved_phones: Pre-trusted phone numbers for this user, e.g. `["+14155551234"]`. + issuing_states: Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + self._client.request( + "post", + "/v3/users/create/", + cast_to=UserDetailItem, + auth="api_key", + body={ + "vendor_data": vendor_data, + "full_name": full_name, + "display_name": display_name, + "date_of_birth": date_of_birth, + "status": status, + "metadata": metadata, + "approved_emails": approved_emails, + "approved_phones": approved_phones, + "issuing_states": issuing_states, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def upload_face( + self, + didit_internal_id: str, + *, + image: str, + comment: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersUploadFaceResponse: + """Upload user face. + + Attach a trusted imported face image to an existing User profile. Use this after creating or retrieving a User by `vendor_data` and reading its `didit_internal_id`. The uploaded face is stored on the User profile and indexed for duplicate detection and face search. It is not added to a face blocklist. + + ``POST /v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/`` + + Args: + didit_internal_id: Didit's internal User UUID, returned as `didit_internal_id` by `GET /v3/users/{vendor_data}/` and `POST /v3/users/create/`. + image: Raw base64-encoded JPG or PNG face image. Do not include a `data:image/...;base64,` prefix. Max decoded size: 2 MB. + comment: Optional operator-visible note for the uploaded face. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersUploadFaceResponse", + self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/", + cast_to=UsersUploadFaceResponse, + path_params={"didit_internal_id": didit_internal_id}, + scope="application", + auth="api_key", + body={"image": image, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def set_ongoing_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + update_all: bool | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_status: Sequence[str] | NotGiven = NOT_GIVEN, + filter_country: str | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + UsersSetOngoingMonitoringResponseVariant1 + | UsersSetOngoingMonitoringResponseVariant2 + | UsersSetOngoingMonitoringResponse202 + ): + """Toggle AML ongoing monitoring for users. + + Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes. + update_all: Apply to every entity of the application (scoped by the filter_* params when present). + filter_search + filter_status + filter_country + filter_date_from + filter_date_to + didit_internal_id_list: Entities to toggle. Required unless update_all is true. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersSetOngoingMonitoringResponseVariant1 | UsersSetOngoingMonitoringResponseVariant2 | UsersSetOngoingMonitoringResponse202", + self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/", + cast_to={ + 200: UsersSetOngoingMonitoringResponseVariant1 + | UsersSetOngoingMonitoringResponseVariant2, + 202: UsersSetOngoingMonitoringResponse202, + }, + scope="application", + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "update_all": update_all, + "filter_search": filter_search, + "filter_status": filter_status, + "filter_country": filter_country, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve_ongoing_monitoring_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersRetrieveOngoingMonitoringJobResponse: + """Get AML ongoing monitoring job. + + Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. + + ``GET /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/`` + + Args: + job_id: Monitoring job UUID returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersRetrieveOngoingMonitoringJobResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/", + cast_to=UsersRetrieveOngoingMonitoringJobResponse, + path_params={"job_id": job_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def list_networks( + self, + vendor_user_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersListNetworksResponse: + """List networks for a user. + + List the networks a vendor user belongs to. + + ``GET /v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/`` + + Args: + vendor_user_uuid: Vendor user UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersListNetworksResponse", + self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/", + cast_to=UsersListNetworksResponse, + path_params={"vendor_user_uuid": vendor_user_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncUsers(AsyncAPIResource): + """The ``users`` resource of the Didit API. + + Methods: ``batch_delete``, ``create``, ``generate_pdf``, ``list``, ``list_networks``, ``retrieve``, ``retrieve_ongoing_monitoring_job``, ``set_aml_monitoring``, ``set_ongoing_monitoring``, ``update``, ``update_status``, ``upload_face``. + """ + + @cached_property + def with_raw_response(self) -> AsyncUsersWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncUsersWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncUsersWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncUsersWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[UserListItem]: + """List users. + + List [users](/entities/users) Didit assembles from KYC sessions sharing the same `vendor_data` (free-form string, NOT a UUID), plus users created via the API. Paginated, ordered by `last_session_at` desc. This endpoint does not support filtering or search parameters - fetch pages and filter client-side. + + ``GET /v3/users/`` + + Args: + limit: Page size. Defaults to 50 when omitted. There is no enforced maximum, but keep pages small for latency. + offset: Zero-based offset of the first record to return. Prefer following the `next` URL from the previous page over computing this by hand. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[UserListItem]", + self._client.request_page( + "get", + "/v3/users/", + cast_to=UsersListResponse, + item_type=UserListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + vendor_data: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Get user. + + Fetch one [user](/entities/users) by `vendor_data` (exact match). Adds `metadata`, `comments`, and `updated_at` to the list view, and switches `tags` to the detailed tag-link shape. + + ``GET /v3/users/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the user - a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the session that first introduced this user, matched exactly as sent. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + await self._client.request( + "get", + "/v3/users/{vendor_data}/", + cast_to=UserDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + vendor_data: str, + *, + full_name: str | None | NotGiven = NOT_GIVEN, + display_name: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, + approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, + issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Update user. + + Partial update of a user: `display_name`, `full_name`, `date_of_birth`, `status`, `metadata`, `approved_emails`/`approved_phones`/`issuing_states`. `metadata` fully replaces. **Note:** unlike [Update User Status](#patch-/v3/users/-vendor_data-/update-status/), changing `status` here does NOT record a status-change activity and does NOT sync the system blocklist - use the dedicated update-status endpoint for `BLOCKED`/unblock flows. + + ``PATCH /v3/users/{vendor_data}/`` + + Args: + vendor_data: Your unique identifier for the user (free-form string, NOT a UUID). + full_name: Full name of the user + display_name: Custom display name for this user + date_of_birth: Date of birth in YYYY-MM-DD format + status: User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint. + metadata: Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys. + approved_emails: Verified email addresses for this user, e.g. `["john@example.com"]`. Fully replaces the stored list. + approved_phones: Verified phone numbers for this user. Fully replaces the stored list. + issuing_states: Issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. Fully replaces the stored list. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + await self._client.request( + "patch", + "/v3/users/{vendor_data}/", + cast_to=UserDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={ + "full_name": full_name, + "display_name": display_name, + "date_of_birth": date_of_birth, + "status": status, + "metadata": metadata, + "approved_emails": approved_emails, + "approved_phones": approved_phones, + "issuing_states": issuing_states, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def generate_pdf( + self, + vendor_data: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Download the full user history PDF report. + + Render and download one compliance-ready PDF containing the full verification history of the user identified by `vendor_data`. It is the same document the Console produces from **User detail → Actions → Download PDF**, so periodic compliance exports no longer need a person clicking in the Console. + + **What the report contains.** A cover page with the profile summary (name, date of birth, issuing states, approved emails and phones), the latest status of every feature, the session counters and an index of the sessions included - followed by the standard report of every included session, in chronological order (oldest first). Each session report is the same document `GET /v3/session/{sessionId}/generate-pdf/` returns for that session. + + **Eligible sessions.** Only User Verification (KYC) sessions in `Approved`, `Declined`, `In Review` or `Kyc Expired` are included; sessions in any other status are skipped. If the user has no session in one of those statuses the endpoint returns `403` - see below. + + **Cap.** The report is capped at the **20 most recent** reportable sessions. When a user has more, the newest 20 are included and the cover page states how many older sessions were left out. Use `GET /v3/sessions/?vendor_data=…` for the complete list as JSON. + + **Branding.** If your application has white-label customization, the cover and every session report are rendered with your logo and privacy-policy URL; otherwise they carry Didit branding. Reports are rendered in English - there is no language parameter. + + **No caching.** Every call re-renders the PDF from the current data, so a report generated after a manual review reflects the reviewer's decision. Two calls for the same user can produce byte-different files - archive the downloaded file if you need an immutable copy. + + **Latency.** Rendering is synchronous and downloads every stored image of every included session, so a user with many media-heavy sessions can take tens of seconds. Use a generous client read timeout (120 s recommended) and stream the body to disk. Generation stops at a server-side time budget instead of running until your client gives up: if that budget is reached, the response is still a valid PDF containing the most recent sessions, and the cover states how many were omitted. + + **Scope.** The user is resolved inside the application that owns the API key. A key can never export a user of another application - such a user does not exist for it and the endpoint returns `404`. + + **Trailing slash.** The canonical route ends with a trailing slash (`…/generate-pdf/`). A request without it is served directly with the same response - there is no `301` redirect, so no client has to follow redirects and `curl` does not need `-L`. Use the slashed URL as in the samples. + + ``GET /v3/users/{vendor_data}/generate-pdf/`` + + Args: + vendor_data: Your unique identifier for the user - a free-form string (NOT a UUID). This is the same value you passed as `vendor_data` when creating the user's sessions, matched exactly as sent. URL-encode it; a value containing `/` cannot be addressed by this route. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "get", + "/v3/users/{vendor_data}/generate-pdf/", + cast_to=bytes, + path_params={"vendor_data": vendor_data}, + auth="api_key", + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def batch_delete( + self, + *, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + delete_all: bool | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersBatchDeleteResponse: + """Batch delete users. + + **Permanently delete** users - the user records are removed permanently. For everyday blocking prefer [Update User Status](#patch-/v3/users/-vendor_data-/update-status/) with `BLOCKED`. Select users with `vendor_data_list`, with `didit_internal_id_list`, or with both at once; or pass `delete_all: true`. The Console's `filter_*` batch params are rejected with a `400` rather than ignored, so a narrowed intent can never turn into a full wipe. + + **Biometric templates:** every image-free biometric template retained for these users after session deletion (see `/v3/biometric-templates/`) is purged before the user is deleted, so the person stops matching in duplicate detection and Face Search. If a template cannot be purged because a biometric store is unavailable, the call returns `503`, no user in the request is deleted, and repeating the request finishes the job. + + ``POST /v3/users/delete/`` + + Args: + vendor_data_list: Your own identifiers to delete, matched exactly as sent. + didit_internal_id_list: Didit internal user IDs to delete - the `didit_internal_id` returned by the create and get endpoints. + delete_all: If `true`, deletes every user in the application. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersBatchDeleteResponse", + await self._client.request( + "post", + "/v3/users/delete/", + cast_to=UsersBatchDeleteResponse, + auth="api_key", + body={ + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + "delete_all": delete_all, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def set_aml_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + vendor_data_list: Sequence[str] | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + UsersSetAmlMonitoringResponseVariant1 + | UsersSetAmlMonitoringResponseVariant2 + | UsersSetAmlMonitoringResponse202 + ): + """Enable or disable AML ongoing monitoring for users. + + Turns AML ongoing monitoring on or off for the selected users, with the same semantics as the Business Console. Exactly one monitored AML screening is kept per user, so a profile that verified several times is monitored and billed once; disabling turns monitoring off across all of the profile's screenings. Users without any AML screening are screened first (billed as a regular AML screening) when they have the full name, date of birth and country needed; otherwise they are reported as `skipped_no_identity_data`. + + Set `estimate_only: true` to preview counts and costs without writing anything. Selections of 20 profiles or fewer resolve inline and return `200` with the completed job; larger selections return `202` and run in the background - poll [Get AML monitoring job](#get-/v3/aml-monitoring/jobs/-job_id-/) with the returned `job_id`. Enabling is refused with `403` when the organization balance does not cover the estimate. Identifiers that match nothing are listed in `unmatched` and never fail the request: a selection that matches nothing completes as an empty job (`total` 0), the same convention as the batch delete endpoints. + + ``POST /v3/users/aml-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes - no screening, no billing, no job. + vendor_data_list: Your own identifiers of the users to toggle, matched exactly as sent. + didit_internal_id_list: Didit internal ids of the users to toggle - the `didit_internal_id` returned by the create, get and list endpoints. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersSetAmlMonitoringResponseVariant1 | UsersSetAmlMonitoringResponseVariant2 | UsersSetAmlMonitoringResponse202", + await self._client.request( + "post", + "/v3/users/aml-monitoring/", + cast_to={ + 200: UsersSetAmlMonitoringResponseVariant1 + | UsersSetAmlMonitoringResponseVariant2, + 202: UsersSetAmlMonitoringResponse202, + }, + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "vendor_data_list": vendor_data_list, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update_status( + self, + vendor_data: str, + *, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Update user status. + + Flip only the lifecycle `status` of a user - `ACTIVE`/`FLAGGED`/`BLOCKED` (NOT session statuses). `BLOCKED` adds the `vendor_data` to the system blocklist; moving away from `BLOCKED` removes that entry. Each change is recorded as a `STATUS_CHANGED` activity on the user. + + ``PATCH /v3/users/{vendor_data}/update-status/`` + + Args: + vendor_data: Your unique identifier for the user. + status: New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + await self._client.request( + "patch", + "/v3/users/{vendor_data}/update-status/", + cast_to=UserDetailItem, + path_params={"vendor_data": vendor_data}, + auth="api_key", + body={"status": status}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + vendor_data: str, + full_name: str | None | NotGiven = NOT_GIVEN, + display_name: str | None | NotGiven = NOT_GIVEN, + date_of_birth: str | _dt.date | None | NotGiven = NOT_GIVEN, + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | NotGiven = NOT_GIVEN, + metadata: Mapping[str, object] | None | NotGiven = NOT_GIVEN, + approved_emails: Sequence[str] | NotGiven = NOT_GIVEN, + approved_phones: Sequence[str] | NotGiven = NOT_GIVEN, + issuing_states: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UserDetailItem: + """Create user. + + Pre-create a [user](/entities/users) by `vendor_data` *without* a verification session. `vendor_data` must be unique among non-deleted users for the application (exact match; conflicts return 400). Not idempotent. + + ``POST /v3/users/create/`` + + Args: + vendor_data: Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent. + full_name: Full legal name of the user. + display_name: Friendly display name shown in the console (takes precedence over `full_name` for UI display). + date_of_birth: Date of birth in `YYYY-MM-DD` format. + status: Initial lifecycle status. Defaults to `ACTIVE`. + metadata: Arbitrary JSON object you attach to the user. Defaults to `{}`. + approved_emails: Pre-trusted email addresses for this user, e.g. `["john@example.com"]`. + approved_phones: Pre-trusted phone numbers for this user, e.g. `["+14155551234"]`. + issuing_states: Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UserDetailItem", + await self._client.request( + "post", + "/v3/users/create/", + cast_to=UserDetailItem, + auth="api_key", + body={ + "vendor_data": vendor_data, + "full_name": full_name, + "display_name": display_name, + "date_of_birth": date_of_birth, + "status": status, + "metadata": metadata, + "approved_emails": approved_emails, + "approved_phones": approved_phones, + "issuing_states": issuing_states, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def upload_face( + self, + didit_internal_id: str, + *, + image: str, + comment: str | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersUploadFaceResponse: + """Upload user face. + + Attach a trusted imported face image to an existing User profile. Use this after creating or retrieving a User by `vendor_data` and reading its `didit_internal_id`. The uploaded face is stored on the User profile and indexed for duplicate detection and face search. It is not added to a face blocklist. + + ``POST /v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/`` + + Args: + didit_internal_id: Didit's internal User UUID, returned as `didit_internal_id` by `GET /v3/users/{vendor_data}/` and `POST /v3/users/create/`. + image: Raw base64-encoded JPG or PNG face image. Do not include a `data:image/...;base64,` prefix. Max decoded size: 2 MB. + comment: Optional operator-visible note for the uploaded face. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersUploadFaceResponse", + await self._client.request( + "post", + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/", + cast_to=UsersUploadFaceResponse, + path_params={"didit_internal_id": didit_internal_id}, + scope="application", + auth="api_key", + body={"image": image, "comment": comment}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def set_ongoing_monitoring( + self, + *, + is_enabled: bool, + estimate_only: bool | NotGiven = NOT_GIVEN, + update_all: bool | NotGiven = NOT_GIVEN, + filter_search: str | NotGiven = NOT_GIVEN, + filter_status: Sequence[str] | NotGiven = NOT_GIVEN, + filter_country: str | NotGiven = NOT_GIVEN, + filter_date_from: str | _dt.date | NotGiven = NOT_GIVEN, + filter_date_to: str | _dt.date | NotGiven = NOT_GIVEN, + didit_internal_id_list: Sequence[str] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> ( + UsersSetOngoingMonitoringResponseVariant1 + | UsersSetOngoingMonitoringResponseVariant2 + | UsersSetOngoingMonitoringResponse202 + ): + """Toggle AML ongoing monitoring for users. + + Enables or disables AML ongoing monitoring at the user level. Exactly one monitored AML screening is kept per user; users without any AML screening (for example imported users) get one created and screened first, billed as a regular AML screening. Set estimate_only to preview counts and costs without writing. Batches of 20 or fewer resolve inline (200, COMPLETED); larger batches return 202 and run asynchronously. + + ``PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/`` + + Args: + is_enabled: true to enable monitoring, false to disable it. + estimate_only: When true, returns the cost estimate and performs no writes. + update_all: Apply to every entity of the application (scoped by the filter_* params when present). + filter_search + filter_status + filter_country + filter_date_from + filter_date_to + didit_internal_id_list: Entities to toggle. Required unless update_all is true. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersSetOngoingMonitoringResponseVariant1 | UsersSetOngoingMonitoringResponseVariant2 | UsersSetOngoingMonitoringResponse202", + await self._client.request( + "patch", + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/", + cast_to={ + 200: UsersSetOngoingMonitoringResponseVariant1 + | UsersSetOngoingMonitoringResponseVariant2, + 202: UsersSetOngoingMonitoringResponse202, + }, + scope="application", + auth="api_key", + body={ + "is_enabled": is_enabled, + "estimate_only": estimate_only, + "update_all": update_all, + "filter_search": filter_search, + "filter_status": filter_status, + "filter_country": filter_country, + "filter_date_from": filter_date_from, + "filter_date_to": filter_date_to, + "didit_internal_id_list": didit_internal_id_list, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve_ongoing_monitoring_job( + self, + job_id: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersRetrieveOngoingMonitoringJobResponse: + """Get AML ongoing monitoring job. + + Progress and per-outcome counters of a monitoring toggle job. Poll every couple of seconds while status is PENDING or PROCESSING. + + ``GET /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/`` + + Args: + job_id: Monitoring job UUID returned by the toggle endpoint. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersRetrieveOngoingMonitoringJobResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/", + cast_to=UsersRetrieveOngoingMonitoringJobResponse, + path_params={"job_id": job_id}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def list_networks( + self, + vendor_user_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> UsersListNetworksResponse: + """List networks for a user. + + List the networks a vendor user belongs to. + + ``GET /v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/`` + + Args: + vendor_user_uuid: Vendor user UUID. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "UsersListNetworksResponse", + await self._client.request( + "get", + "/v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/", + cast_to=UsersListNetworksResponse, + path_params={"vendor_user_uuid": vendor_user_uuid}, + scope="application", + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class UsersWithRawResponse: + def __init__(self, resource: Users) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.update = to_raw_response_wrapper(resource.update) + self.generate_pdf = to_raw_response_wrapper(resource.generate_pdf) + self.batch_delete = to_raw_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = to_raw_response_wrapper(resource.set_aml_monitoring) + self.update_status = to_raw_response_wrapper(resource.update_status) + self.create = to_raw_response_wrapper(resource.create) + self.upload_face = to_raw_response_wrapper(resource.upload_face) + self.set_ongoing_monitoring = to_raw_response_wrapper(resource.set_ongoing_monitoring) + self.retrieve_ongoing_monitoring_job = to_raw_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.list_networks = to_raw_response_wrapper(resource.list_networks) + + +class AsyncUsersWithRawResponse: + def __init__(self, resource: AsyncUsers) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.update = async_to_raw_response_wrapper(resource.update) + self.generate_pdf = async_to_raw_response_wrapper(resource.generate_pdf) + self.batch_delete = async_to_raw_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = async_to_raw_response_wrapper(resource.set_aml_monitoring) + self.update_status = async_to_raw_response_wrapper(resource.update_status) + self.create = async_to_raw_response_wrapper(resource.create) + self.upload_face = async_to_raw_response_wrapper(resource.upload_face) + self.set_ongoing_monitoring = async_to_raw_response_wrapper(resource.set_ongoing_monitoring) + self.retrieve_ongoing_monitoring_job = async_to_raw_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.list_networks = async_to_raw_response_wrapper(resource.list_networks) + + +class UsersWithStreamingResponse: + def __init__(self, resource: Users) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.update = to_streamed_response_wrapper(resource.update) + self.generate_pdf = to_streamed_response_wrapper(resource.generate_pdf) + self.batch_delete = to_streamed_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = to_streamed_response_wrapper(resource.set_aml_monitoring) + self.update_status = to_streamed_response_wrapper(resource.update_status) + self.create = to_streamed_response_wrapper(resource.create) + self.upload_face = to_streamed_response_wrapper(resource.upload_face) + self.set_ongoing_monitoring = to_streamed_response_wrapper(resource.set_ongoing_monitoring) + self.retrieve_ongoing_monitoring_job = to_streamed_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.list_networks = to_streamed_response_wrapper(resource.list_networks) + + +class AsyncUsersWithStreamingResponse: + def __init__(self, resource: AsyncUsers) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.update = async_to_streamed_response_wrapper(resource.update) + self.generate_pdf = async_to_streamed_response_wrapper(resource.generate_pdf) + self.batch_delete = async_to_streamed_response_wrapper(resource.batch_delete) + self.set_aml_monitoring = async_to_streamed_response_wrapper(resource.set_aml_monitoring) + self.update_status = async_to_streamed_response_wrapper(resource.update_status) + self.create = async_to_streamed_response_wrapper(resource.create) + self.upload_face = async_to_streamed_response_wrapper(resource.upload_face) + self.set_ongoing_monitoring = async_to_streamed_response_wrapper( + resource.set_ongoing_monitoring + ) + self.retrieve_ongoing_monitoring_job = async_to_streamed_response_wrapper( + resource.retrieve_ongoing_monitoring_job + ) + self.list_networks = async_to_streamed_response_wrapper(resource.list_networks) diff --git a/src/didit/resources/wallet_screening.py b/src/didit/resources/wallet_screening.py new file mode 100644 index 0000000..78c66a3 --- /dev/null +++ b/src/didit/resources/wallet_screening.py @@ -0,0 +1,468 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``wallet_screening`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import Literal, Mapping, Sequence, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.wallet_screening_result import WalletScreeningResult +from didit.types.wallet_screening.screen_pdf_params import ( + WalletScreeningScreenPdfParamsCounterpartyConnections, + WalletScreeningScreenPdfParamsDestinationOfFunds, + WalletScreeningScreenPdfParamsRiskFactors, + WalletScreeningScreenPdfParamsSourceOfFunds, +) + +__all__ = [ + "WalletScreening", + "AsyncWalletScreening", + "WalletScreeningWithRawResponse", + "AsyncWalletScreeningWithRawResponse", + "WalletScreeningWithStreamingResponse", + "AsyncWalletScreeningWithStreamingResponse", +] + + +class WalletScreening(SyncAPIResource): + """The ``wallet_screening`` resource of the Didit API. + + Methods: ``screen``, ``screen_pdf``. + """ + + @cached_property + def with_raw_response(self) -> WalletScreeningWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return WalletScreeningWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WalletScreeningWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return WalletScreeningWithStreamingResponse(self) + + def screen( + self, + *, + wallet_address: str, + blockchain: Literal[ + "BTC", + "LIGHTNING", + "ETH", + "LTC", + "XRP", + "BCH", + "DOGE", + "TRX", + "SOL", + "MATIC", + "BNB", + "USDT", + "USDC", + "BASE", + "OP", + "ARB", + ] + | str, + direction: Literal["inbound", "outbound", "deposit", "withdrawal"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletScreeningResult: + """Screen a wallet address. + + Run on-demand AML screening for a single crypto wallet/address without creating a transaction. Resolves your application's configured blockchain analytics provider, screens the address synchronously, and returns the normalised risk result. LIGHTNING is available only with an Elliptic or Chainalysis BYOK connection added through Marketplace. Nothing is written to the transactions table. One AML monitoring usage is billed per successful screening (sandbox applications are not billed). + + ``POST /v3/wallet-screening/`` + + Args: + wallet_address: The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`. + blockchain: Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`. + direction: Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletScreeningResult", + self._client.request( + "post", + "/v3/wallet-screening/", + cast_to=WalletScreeningResult, + auth="api_key", + body={ + "wallet_address": wallet_address, + "blockchain": blockchain, + "direction": direction, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def screen_pdf( + self, + *, + provider: str, + screening_type: Literal["WALLET_SCREENING"] | str, + risk_score: int, + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str, + status: Literal["SCREENED", "PENDING", "ERROR"] | str, + summary: str, + wallet_address: str, + blockchain: str, + source_of_funds: Sequence[WalletScreeningScreenPdfParamsSourceOfFunds], + destination_of_funds: Sequence[WalletScreeningScreenPdfParamsDestinationOfFunds], + counterparty_connections: Sequence[WalletScreeningScreenPdfParamsCounterpartyConnections], + sanctions_hit: bool, + dominant_risk_category: str | None, + report_signature: str, + transaction_hash: str | None | NotGiven = NOT_GIVEN, + block_timestamp: str | None | NotGiven = NOT_GIVEN, + total_value_usd: float | None | NotGiven = NOT_GIVEN, + fee_usd: float | None | NotGiven = NOT_GIVEN, + sender_rows: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + receiver_rows: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + transaction_flows: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + transaction_flows_status: str | None | NotGiven = NOT_GIVEN, + transaction_flows_error: str | None | NotGiven = NOT_GIVEN, + network_nodes: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + network_edges: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + pep_counterparty: bool | None | NotGiven = NOT_GIVEN, + risk_factors: Sequence[WalletScreeningScreenPdfParamsRiskFactors] | NotGiven = NOT_GIVEN, + raw_response: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Render a wallet-screening result as a PDF. + + Stateless companion to `POST /v3/wallet-screening/`: repost the exact JSON body that endpoint returned - including the `report_signature` it added - and get back a Didit-branded PDF of the same result. No provider is called and nothing is billed or persisted; this only re-renders a result Didit already produced. The signature ties the PDF to an unmodified result: editing `risk_score`, `severity`, `sanctions_hit`, or any other signed field before reposting fails with `400`. + + ``POST /v3/wallet-screening/pdf/`` + + Args: + provider: Provider that performed the screening (e.g. `merklescience`, `crystal`). + screening_type: Always `WALLET_SCREENING` for this endpoint. + risk_score: Normalised 0-100 risk score. Higher means greater exposure to risky entities. Merkle Science grades an address 0-5, which maps to 0, 15, 35, 55, 75 or 95. + severity: Risk bucket derived from `risk_score`: `0-9` UNKNOWN, `10-39` LOW, `40-69` MEDIUM, `70-89` HIGH, `90-100` CRITICAL. `UNKNOWN` is the lowest band, not a separate no-data state: `risk_score` 0 (the common clean-address case) means no adverse assessment, while a non-zero score in the 1-9 range is a real but sub-LOW signal - read the `risk_score`, not just the band. Never treat `UNKNOWN` as an affirmative low-risk or clear rating; do not display it as a pass. + status: Screening outcome status. + summary: Human-readable summary of the screening result. + wallet_address: The screened address, echoed back. + blockchain: The blockchain that was screened. + source_of_funds: Where the address received funds from, attributed by entity. Each entry is an exposure breakdown. + destination_of_funds: Where the address sent funds to, attributed by entity. Same item shape as `source_of_funds` with `exposure_direction` = `outgoing`. + counterparty_connections: Entities the provider attributes the screened address to. For Merkle Science these come from the address's own owner and user tags, so the same entity can appear twice, and they carry no fund flows: `received_usd`, `sent_usd`, `received_hops` and `sent_hops` are `null` and `percentage` is 0. Do not route on `risk_level` or `is_direct` here: use `sanctions_hit`, `dominant_risk_category` and `risk_factors[].is_high_risk` for the verdict, and `exposure_type` in `source_of_funds`/`destination_of_funds` for directness. + sanctions_hit: True if the address has direct or indirect sanctions exposure. + dominant_risk_category: Highest-weighted high-risk category, or `null` when none is dominant (e.g. `sanctioned`, `mixer`, `stolen_funds`). + report_signature: Opaque HMAC-SHA256 signature over this exact result, scoped to your application. Not meaningful on its own - repost the full response body, unmodified and including this field, to `POST /v3/wallet-screening/pdf/` to render it as a PDF. The PDF endpoint rejects the request with `400` if `report_signature` is missing or if any signed field (e.g. `risk_score`, `severity`, `sanctions_hit`) was edited before repost. + transaction_hash: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + block_timestamp: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + total_value_usd: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + fee_usd: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + sender_rows: Transaction screening field, shared with the result shape. Always empty on a wallet screening result. + receiver_rows: Transaction screening field, shared with the result shape. Always empty on a wallet screening result. + transaction_flows: Transaction screening field, shared with the result shape. Always empty on a wallet screening result. + transaction_flows_status: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + transaction_flows_error: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + network_nodes: Exposure graph nodes for transaction screening. Empty on a wallet screening result. + network_edges: Exposure graph edges for transaction screening. Empty on a wallet screening result. + pep_counterparty: Politically exposed person exposure. `null` when no PEP finding is available, which is every result today because no provider reports one; `false` only for an explicit negative screening result. Do not read `null` as a completed PEP check. + risk_factors: Ranked explanation of `risk_score`: what produced this verdict. Providers grade an address on their own model and return only a verdict (Merkle Science returns an integer 0-5), so the score alone is not actionable. Ordered high-risk first, then by the value that moved through each driver, capped at the five strongest; `source_of_funds` and `destination_of_funds` always carry the complete breakdown. A high `risk_score` with no `is_high_risk` entry means the provider reported no sanctions or attributed-entity match and graded the address purely on its own exposure model - `summary` states this explicitly. + raw_response: The provider's own payload. Its shape is provider-specific and can change without notice: read the normalised fields instead of parsing it. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + self._client.request( + "post", + "/v3/wallet-screening/pdf/", + cast_to=bytes, + auth="api_key", + body={ + "provider": provider, + "screening_type": screening_type, + "risk_score": risk_score, + "severity": severity, + "status": status, + "summary": summary, + "wallet_address": wallet_address, + "blockchain": blockchain, + "source_of_funds": source_of_funds, + "destination_of_funds": destination_of_funds, + "counterparty_connections": counterparty_connections, + "sanctions_hit": sanctions_hit, + "dominant_risk_category": dominant_risk_category, + "report_signature": report_signature, + "transaction_hash": transaction_hash, + "block_timestamp": block_timestamp, + "total_value_usd": total_value_usd, + "fee_usd": fee_usd, + "sender_rows": sender_rows, + "receiver_rows": receiver_rows, + "transaction_flows": transaction_flows, + "transaction_flows_status": transaction_flows_status, + "transaction_flows_error": transaction_flows_error, + "network_nodes": network_nodes, + "network_edges": network_edges, + "pep_counterparty": pep_counterparty, + "risk_factors": risk_factors, + "raw_response": raw_response, + }, + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncWalletScreening(AsyncAPIResource): + """The ``wallet_screening`` resource of the Didit API. + + Methods: ``screen``, ``screen_pdf``. + """ + + @cached_property + def with_raw_response(self) -> AsyncWalletScreeningWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncWalletScreeningWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWalletScreeningWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncWalletScreeningWithStreamingResponse(self) + + async def screen( + self, + *, + wallet_address: str, + blockchain: Literal[ + "BTC", + "LIGHTNING", + "ETH", + "LTC", + "XRP", + "BCH", + "DOGE", + "TRX", + "SOL", + "MATIC", + "BNB", + "USDT", + "USDC", + "BASE", + "OP", + "ARB", + ] + | str, + direction: Literal["inbound", "outbound", "deposit", "withdrawal"] + | str + | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WalletScreeningResult: + """Screen a wallet address. + + Run on-demand AML screening for a single crypto wallet/address without creating a transaction. Resolves your application's configured blockchain analytics provider, screens the address synchronously, and returns the normalised risk result. LIGHTNING is available only with an Elliptic or Chainalysis BYOK connection added through Marketplace. Nothing is written to the transactions table. One AML monitoring usage is billed per successful screening (sandbox applications are not billed). + + ``POST /v3/wallet-screening/`` + + Args: + wallet_address: The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`. + blockchain: Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`. + direction: Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WalletScreeningResult", + await self._client.request( + "post", + "/v3/wallet-screening/", + cast_to=WalletScreeningResult, + auth="api_key", + body={ + "wallet_address": wallet_address, + "blockchain": blockchain, + "direction": direction, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def screen_pdf( + self, + *, + provider: str, + screening_type: Literal["WALLET_SCREENING"] | str, + risk_score: int, + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str, + status: Literal["SCREENED", "PENDING", "ERROR"] | str, + summary: str, + wallet_address: str, + blockchain: str, + source_of_funds: Sequence[WalletScreeningScreenPdfParamsSourceOfFunds], + destination_of_funds: Sequence[WalletScreeningScreenPdfParamsDestinationOfFunds], + counterparty_connections: Sequence[WalletScreeningScreenPdfParamsCounterpartyConnections], + sanctions_hit: bool, + dominant_risk_category: str | None, + report_signature: str, + transaction_hash: str | None | NotGiven = NOT_GIVEN, + block_timestamp: str | None | NotGiven = NOT_GIVEN, + total_value_usd: float | None | NotGiven = NOT_GIVEN, + fee_usd: float | None | NotGiven = NOT_GIVEN, + sender_rows: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + receiver_rows: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + transaction_flows: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + transaction_flows_status: str | None | NotGiven = NOT_GIVEN, + transaction_flows_error: str | None | NotGiven = NOT_GIVEN, + network_nodes: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + network_edges: Sequence[Mapping[str, object]] | NotGiven = NOT_GIVEN, + pep_counterparty: bool | None | NotGiven = NOT_GIVEN, + risk_factors: Sequence[WalletScreeningScreenPdfParamsRiskFactors] | NotGiven = NOT_GIVEN, + raw_response: Mapping[str, object] | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> bytes: + """Render a wallet-screening result as a PDF. + + Stateless companion to `POST /v3/wallet-screening/`: repost the exact JSON body that endpoint returned - including the `report_signature` it added - and get back a Didit-branded PDF of the same result. No provider is called and nothing is billed or persisted; this only re-renders a result Didit already produced. The signature ties the PDF to an unmodified result: editing `risk_score`, `severity`, `sanctions_hit`, or any other signed field before reposting fails with `400`. + + ``POST /v3/wallet-screening/pdf/`` + + Args: + provider: Provider that performed the screening (e.g. `merklescience`, `crystal`). + screening_type: Always `WALLET_SCREENING` for this endpoint. + risk_score: Normalised 0-100 risk score. Higher means greater exposure to risky entities. Merkle Science grades an address 0-5, which maps to 0, 15, 35, 55, 75 or 95. + severity: Risk bucket derived from `risk_score`: `0-9` UNKNOWN, `10-39` LOW, `40-69` MEDIUM, `70-89` HIGH, `90-100` CRITICAL. `UNKNOWN` is the lowest band, not a separate no-data state: `risk_score` 0 (the common clean-address case) means no adverse assessment, while a non-zero score in the 1-9 range is a real but sub-LOW signal - read the `risk_score`, not just the band. Never treat `UNKNOWN` as an affirmative low-risk or clear rating; do not display it as a pass. + status: Screening outcome status. + summary: Human-readable summary of the screening result. + wallet_address: The screened address, echoed back. + blockchain: The blockchain that was screened. + source_of_funds: Where the address received funds from, attributed by entity. Each entry is an exposure breakdown. + destination_of_funds: Where the address sent funds to, attributed by entity. Same item shape as `source_of_funds` with `exposure_direction` = `outgoing`. + counterparty_connections: Entities the provider attributes the screened address to. For Merkle Science these come from the address's own owner and user tags, so the same entity can appear twice, and they carry no fund flows: `received_usd`, `sent_usd`, `received_hops` and `sent_hops` are `null` and `percentage` is 0. Do not route on `risk_level` or `is_direct` here: use `sanctions_hit`, `dominant_risk_category` and `risk_factors[].is_high_risk` for the verdict, and `exposure_type` in `source_of_funds`/`destination_of_funds` for directness. + sanctions_hit: True if the address has direct or indirect sanctions exposure. + dominant_risk_category: Highest-weighted high-risk category, or `null` when none is dominant (e.g. `sanctioned`, `mixer`, `stolen_funds`). + report_signature: Opaque HMAC-SHA256 signature over this exact result, scoped to your application. Not meaningful on its own - repost the full response body, unmodified and including this field, to `POST /v3/wallet-screening/pdf/` to render it as a PDF. The PDF endpoint rejects the request with `400` if `report_signature` is missing or if any signed field (e.g. `risk_score`, `severity`, `sanctions_hit`) was edited before repost. + transaction_hash: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + block_timestamp: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + total_value_usd: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + fee_usd: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + sender_rows: Transaction screening field, shared with the result shape. Always empty on a wallet screening result. + receiver_rows: Transaction screening field, shared with the result shape. Always empty on a wallet screening result. + transaction_flows: Transaction screening field, shared with the result shape. Always empty on a wallet screening result. + transaction_flows_status: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + transaction_flows_error: Transaction screening field, shared with the result shape. Always `null` on a wallet screening result. + network_nodes: Exposure graph nodes for transaction screening. Empty on a wallet screening result. + network_edges: Exposure graph edges for transaction screening. Empty on a wallet screening result. + pep_counterparty: Politically exposed person exposure. `null` when no PEP finding is available, which is every result today because no provider reports one; `false` only for an explicit negative screening result. Do not read `null` as a completed PEP check. + risk_factors: Ranked explanation of `risk_score`: what produced this verdict. Providers grade an address on their own model and return only a verdict (Merkle Science returns an integer 0-5), so the score alone is not actionable. Ordered high-risk first, then by the value that moved through each driver, capped at the five strongest; `source_of_funds` and `destination_of_funds` always carry the complete breakdown. A high `risk_score` with no `is_high_risk` entry means the provider reported no sanctions or attributed-entity match and graded the address purely on its own exposure model - `summary` states this explicitly. + raw_response: The provider's own payload. Its shape is provider-specific and can change without notice: read the normalised fields instead of parsing it. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "bytes", + await self._client.request( + "post", + "/v3/wallet-screening/pdf/", + cast_to=bytes, + auth="api_key", + body={ + "provider": provider, + "screening_type": screening_type, + "risk_score": risk_score, + "severity": severity, + "status": status, + "summary": summary, + "wallet_address": wallet_address, + "blockchain": blockchain, + "source_of_funds": source_of_funds, + "destination_of_funds": destination_of_funds, + "counterparty_connections": counterparty_connections, + "sanctions_hit": sanctions_hit, + "dominant_risk_category": dominant_risk_category, + "report_signature": report_signature, + "transaction_hash": transaction_hash, + "block_timestamp": block_timestamp, + "total_value_usd": total_value_usd, + "fee_usd": fee_usd, + "sender_rows": sender_rows, + "receiver_rows": receiver_rows, + "transaction_flows": transaction_flows, + "transaction_flows_status": transaction_flows_status, + "transaction_flows_error": transaction_flows_error, + "network_nodes": network_nodes, + "network_edges": network_edges, + "pep_counterparty": pep_counterparty, + "risk_factors": risk_factors, + "raw_response": raw_response, + }, + accept="application/pdf, application/json", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class WalletScreeningWithRawResponse: + def __init__(self, resource: WalletScreening) -> None: + self._resource = resource + self.screen = to_raw_response_wrapper(resource.screen) + self.screen_pdf = to_raw_response_wrapper(resource.screen_pdf) + + +class AsyncWalletScreeningWithRawResponse: + def __init__(self, resource: AsyncWalletScreening) -> None: + self._resource = resource + self.screen = async_to_raw_response_wrapper(resource.screen) + self.screen_pdf = async_to_raw_response_wrapper(resource.screen_pdf) + + +class WalletScreeningWithStreamingResponse: + def __init__(self, resource: WalletScreening) -> None: + self._resource = resource + self.screen = to_streamed_response_wrapper(resource.screen) + self.screen_pdf = to_streamed_response_wrapper(resource.screen_pdf) + + +class AsyncWalletScreeningWithStreamingResponse: + def __init__(self, resource: AsyncWalletScreening) -> None: + self._resource = resource + self.screen = async_to_streamed_response_wrapper(resource.screen) + self.screen_pdf = async_to_streamed_response_wrapper(resource.screen_pdf) diff --git a/src/didit/resources/webhooks/__init__.py b/src/didit/resources/webhooks/__init__.py new file mode 100644 index 0000000..cb7e1d4 --- /dev/null +++ b/src/didit/resources/webhooks/__init__.py @@ -0,0 +1,121 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``webhooks`` resource.""" + +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit.webhooks._mixin import WebhooksMixin + +if TYPE_CHECKING: + from didit.resources.webhooks.destinations import ( + AsyncWebhooksDestinations, + AsyncWebhooksDestinationsWithRawResponse, + AsyncWebhooksDestinationsWithStreamingResponse, + WebhooksDestinations, + WebhooksDestinationsWithRawResponse, + WebhooksDestinationsWithStreamingResponse, + ) + +__all__ = [ + "Webhooks", + "AsyncWebhooks", + "WebhooksWithRawResponse", + "AsyncWebhooksWithRawResponse", + "WebhooksWithStreamingResponse", + "AsyncWebhooksWithStreamingResponse", +] + + +class Webhooks(WebhooksMixin, SyncAPIResource): + """The ``webhooks`` resource of the Didit API. + + Nested resources: ``destinations``. + """ + + @cached_property + def destinations(self) -> WebhooksDestinations: + from didit.resources.webhooks.destinations import WebhooksDestinations + + return WebhooksDestinations(self._client) + + @cached_property + def with_raw_response(self) -> WebhooksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return WebhooksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WebhooksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return WebhooksWithStreamingResponse(self) + + +class AsyncWebhooks(WebhooksMixin, AsyncAPIResource): + """The ``webhooks`` resource of the Didit API. + + Nested resources: ``destinations``. + """ + + @cached_property + def destinations(self) -> AsyncWebhooksDestinations: + from didit.resources.webhooks.destinations import AsyncWebhooksDestinations + + return AsyncWebhooksDestinations(self._client) + + @cached_property + def with_raw_response(self) -> AsyncWebhooksWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncWebhooksWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWebhooksWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncWebhooksWithStreamingResponse(self) + + +class WebhooksWithRawResponse: + def __init__(self, resource: Webhooks) -> None: + self._resource = resource + + @cached_property + def destinations(self) -> WebhooksDestinationsWithRawResponse: + from didit.resources.webhooks.destinations import WebhooksDestinationsWithRawResponse + + return WebhooksDestinationsWithRawResponse(self._resource.destinations) + + +class AsyncWebhooksWithRawResponse: + def __init__(self, resource: AsyncWebhooks) -> None: + self._resource = resource + + @cached_property + def destinations(self) -> AsyncWebhooksDestinationsWithRawResponse: + from didit.resources.webhooks.destinations import AsyncWebhooksDestinationsWithRawResponse + + return AsyncWebhooksDestinationsWithRawResponse(self._resource.destinations) + + +class WebhooksWithStreamingResponse: + def __init__(self, resource: Webhooks) -> None: + self._resource = resource + + @cached_property + def destinations(self) -> WebhooksDestinationsWithStreamingResponse: + from didit.resources.webhooks.destinations import WebhooksDestinationsWithStreamingResponse + + return WebhooksDestinationsWithStreamingResponse(self._resource.destinations) + + +class AsyncWebhooksWithStreamingResponse: + def __init__(self, resource: AsyncWebhooks) -> None: + self._resource = resource + + @cached_property + def destinations(self) -> AsyncWebhooksDestinationsWithStreamingResponse: + from didit.resources.webhooks.destinations import ( + AsyncWebhooksDestinationsWithStreamingResponse, + ) + + return AsyncWebhooksDestinationsWithStreamingResponse(self._resource.destinations) diff --git a/src/didit/resources/webhooks/destinations.py b/src/didit/resources/webhooks/destinations.py new file mode 100644 index 0000000..ff9f549 --- /dev/null +++ b/src/didit/resources/webhooks/destinations.py @@ -0,0 +1,533 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``webhooks.destinations`` resource.""" + +from __future__ import annotations + +import builtins +from functools import cached_property +from typing import Literal, cast + +import httpx + +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEventsParam +from didit.types.webhooks.destinations.create_response import WebhooksDestinationsCreateResponse +from didit.types.webhooks.destinations.list_response import WebhooksDestinationsListResponseItem +from didit.types.webhooks.destinations.retrieve_response import WebhooksDestinationsRetrieveResponse +from didit.types.webhooks.destinations.update_response import WebhooksDestinationsUpdateResponse + +__all__ = [ + "WebhooksDestinations", + "AsyncWebhooksDestinations", + "WebhooksDestinationsWithRawResponse", + "AsyncWebhooksDestinationsWithRawResponse", + "WebhooksDestinationsWithStreamingResponse", + "AsyncWebhooksDestinationsWithStreamingResponse", +] + + +class WebhooksDestinations(SyncAPIResource): + """The ``webhooks.destinations`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> WebhooksDestinationsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return WebhooksDestinationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WebhooksDestinationsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return WebhooksDestinationsWithStreamingResponse(self) + + def list( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[WebhooksDestinationsListResponseItem]: + """List webhook destinations. + + List all webhook destinations (unpaginated array, oldest first). Each row includes URL, subscribed events, and delivery health metrics. The signing secret is NOT included in the list - fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`. + + ``GET /v3/webhook/destinations/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[WebhooksDestinationsListResponseItem]", + self._client.request( + "get", + "/v3/webhook/destinations/", + cast_to=builtins.list[WebhooksDestinationsListResponseItem], + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + label: str, + url: str, + enabled: bool | NotGiven = NOT_GIVEN, + webhook_version: Literal["v1", "v2", "v3"] | str | NotGiven = NOT_GIVEN, + subscribed_events: WebhookSubscribedEventsParam | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsCreateResponse: + """Create webhook destination. + + Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among non-deleted destinations. + + ``POST /v3/webhook/destinations/`` + + Args: + label: Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. "Prod", "Staging mirror"). + url: HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint - Didit retries failed deliveries on a backoff schedule. + enabled: When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked. + webhook_version: Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations. + subscribed_events: Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsCreateResponse", + self._client.request( + "post", + "/v3/webhook/destinations/", + cast_to=WebhooksDestinationsCreateResponse, + auth="api_key", + body={ + "label": label, + "url": url, + "enabled": enabled, + "webhook_version": webhook_version, + "subscribed_events": subscribed_events, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + destination_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsRetrieveResponse: + """Get webhook destination. + + Retrieve one webhook destination with its `secret_shared_key` signing secret and delivery health summary. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string. + + ``GET /v3/webhook/destinations/{destination_uuid}/`` + + Args: + destination_uuid: UUID of the destination (the `uuid` returned from create / list). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsRetrieveResponse", + self._client.request( + "get", + "/v3/webhook/destinations/{destination_uuid}/", + cast_to=WebhooksDestinationsRetrieveResponse, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + destination_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete webhook destination. + + Delete a webhook destination. Future events stop being pushed to its URL. Past delivery records are not affected and stay queryable from the Business Console, so deleting a destination is not a way to remove your delivery history. Calling again returns 404. + + ``DELETE /v3/webhook/destinations/{destination_uuid}/`` + + Args: + destination_uuid: UUID of the destination to delete. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/webhook/destinations/{destination_uuid}/", + cast_to=None, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + destination_uuid: str, + *, + label: str | NotGiven = NOT_GIVEN, + url: str | NotGiven = NOT_GIVEN, + enabled: bool | NotGiven = NOT_GIVEN, + webhook_version: Literal["v1", "v2", "v3"] | str | NotGiven = NOT_GIVEN, + subscribed_events: WebhookSubscribedEventsParam | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsUpdateResponse: + """Update webhook destination. + + Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged** - fetch first, append, resend. Secret is not rotated. + + ``PATCH /v3/webhook/destinations/{destination_uuid}/`` + + Args: + destination_uuid: UUID of the destination to update. + label: New human-readable name. + url: New target URL. Must be unique within the application. + enabled: Set to `false` to pause deliveries to this destination without deleting it. + webhook_version: New payload schema version for future deliveries. + subscribed_events: Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsUpdateResponse", + self._client.request( + "patch", + "/v3/webhook/destinations/{destination_uuid}/", + cast_to=WebhooksDestinationsUpdateResponse, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + body={ + "label": label, + "url": url, + "enabled": enabled, + "webhook_version": webhook_version, + "subscribed_events": subscribed_events, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncWebhooksDestinations(AsyncAPIResource): + """The ``webhooks.destinations`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncWebhooksDestinationsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncWebhooksDestinationsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWebhooksDestinationsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncWebhooksDestinationsWithStreamingResponse(self) + + async def list( + self, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.list[WebhooksDestinationsListResponseItem]: + """List webhook destinations. + + List all webhook destinations (unpaginated array, oldest first). Each row includes URL, subscribed events, and delivery health metrics. The signing secret is NOT included in the list - fetch a single destination (`GET /v3/webhook/destinations/{destination_uuid}/`) to read `secret_shared_key`. + + ``GET /v3/webhook/destinations/`` + + Args: + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.list[WebhooksDestinationsListResponseItem]", + await self._client.request( + "get", + "/v3/webhook/destinations/", + cast_to=builtins.list[WebhooksDestinationsListResponseItem], + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + label: str, + url: str, + enabled: bool | NotGiven = NOT_GIVEN, + webhook_version: Literal["v1", "v2", "v3"] | str | NotGiven = NOT_GIVEN, + subscribed_events: WebhookSubscribedEventsParam | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsCreateResponse: + """Create webhook destination. + + Create a webhook destination. The response includes the auto-generated `secret_shared_key` HMAC signing secret (also readable later via `GET /v3/webhook/destinations/{destination_uuid}/`). Always send `subscribed_events` with at least one event (no wildcards): an explicit empty list is rejected with 400, and omitting the field creates a destination subscribed to nothing, which never receives a webhook. The `(application, url)` pair must be unique among non-deleted destinations. + + ``POST /v3/webhook/destinations/`` + + Args: + label: Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. "Prod", "Staging mirror"). + url: HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint - Didit retries failed deliveries on a backoff schedule. + enabled: When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked. + webhook_version: Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations. + subscribed_events: Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsCreateResponse", + await self._client.request( + "post", + "/v3/webhook/destinations/", + cast_to=WebhooksDestinationsCreateResponse, + auth="api_key", + body={ + "label": label, + "url": url, + "enabled": enabled, + "webhook_version": webhook_version, + "subscribed_events": subscribed_events, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + destination_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsRetrieveResponse: + """Get webhook destination. + + Retrieve one webhook destination with its `secret_shared_key` signing secret and delivery health summary. API-key callers always receive the secret in full; Console users without `read:webhooks` see an empty string. + + ``GET /v3/webhook/destinations/{destination_uuid}/`` + + Args: + destination_uuid: UUID of the destination (the `uuid` returned from create / list). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsRetrieveResponse", + await self._client.request( + "get", + "/v3/webhook/destinations/{destination_uuid}/", + cast_to=WebhooksDestinationsRetrieveResponse, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + destination_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete webhook destination. + + Delete a webhook destination. Future events stop being pushed to its URL. Past delivery records are not affected and stay queryable from the Business Console, so deleting a destination is not a way to remove your delivery history. Calling again returns 404. + + ``DELETE /v3/webhook/destinations/{destination_uuid}/`` + + Args: + destination_uuid: UUID of the destination to delete. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/webhook/destinations/{destination_uuid}/", + cast_to=None, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + destination_uuid: str, + *, + label: str | NotGiven = NOT_GIVEN, + url: str | NotGiven = NOT_GIVEN, + enabled: bool | NotGiven = NOT_GIVEN, + webhook_version: Literal["v1", "v2", "v3"] | str | NotGiven = NOT_GIVEN, + subscribed_events: WebhookSubscribedEventsParam | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> WebhooksDestinationsUpdateResponse: + """Update webhook destination. + + Update a webhook destination. **`subscribed_events` is REPLACED wholesale, not merged** - fetch first, append, resend. Secret is not rotated. + + ``PATCH /v3/webhook/destinations/{destination_uuid}/`` + + Args: + destination_uuid: UUID of the destination to update. + label: New human-readable name. + url: New target URL. Must be unique within the application. + enabled: Set to `false` to pause deliveries to this destination without deleting it. + webhook_version: New payload schema version for future deliveries. + subscribed_events: Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "WebhooksDestinationsUpdateResponse", + await self._client.request( + "patch", + "/v3/webhook/destinations/{destination_uuid}/", + cast_to=WebhooksDestinationsUpdateResponse, + path_params={"destination_uuid": destination_uuid}, + auth="api_key", + body={ + "label": label, + "url": url, + "enabled": enabled, + "webhook_version": webhook_version, + "subscribed_events": subscribed_events, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class WebhooksDestinationsWithRawResponse: + def __init__(self, resource: WebhooksDestinations) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncWebhooksDestinationsWithRawResponse: + def __init__(self, resource: AsyncWebhooksDestinations) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + + +class WebhooksDestinationsWithStreamingResponse: + def __init__(self, resource: WebhooksDestinations) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncWebhooksDestinationsWithStreamingResponse: + def __init__(self, resource: AsyncWebhooksDestinations) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/resources/workflows.py b/src/didit/resources/workflows.py new file mode 100644 index 0000000..c31d2f8 --- /dev/null +++ b/src/didit/resources/workflows.py @@ -0,0 +1,616 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""The ``workflows`` resource.""" + +from __future__ import annotations + +import builtins +from functools import cached_property +from typing import Literal, Sequence, cast + +import httpx + +from didit._pagination import AsyncPaginator, SyncPage +from didit._resource import AsyncAPIResource, SyncAPIResource +from didit._response import ( + async_to_raw_response_wrapper, + async_to_streamed_response_wrapper, + to_raw_response_wrapper, + to_streamed_response_wrapper, +) +from didit._types import NOT_GIVEN, Body, Headers, NotGiven, Query +from didit.types.components.simple_workflow_feature import SimpleWorkflowFeature +from didit.types.components.workflow_list_item import WorkflowListItem +from didit.types.workflows.list_response import WorkflowsListResponse + +__all__ = [ + "Workflows", + "AsyncWorkflows", + "WorkflowsWithRawResponse", + "AsyncWorkflowsWithRawResponse", + "WorkflowsWithStreamingResponse", + "AsyncWorkflowsWithStreamingResponse", +] + + +class Workflows(SyncAPIResource): + """The ``workflows`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> WorkflowsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return WorkflowsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> WorkflowsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return WorkflowsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> SyncPage[WorkflowListItem]: + """List workflows. + + List your workflows, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `workflow_id` is the stable group ID and new sessions use the latest `published` version. + + ``GET /v3/workflows/`` + + Args: + limit: Page size (number of workflows per page). + offset: Number of rows to skip before the first returned workflow. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "SyncPage[WorkflowListItem]", + self._client.request_page( + "get", + "/v3/workflows/", + cast_to=WorkflowsListResponse, + item_type=WorkflowListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def create( + self, + *, + features: Sequence[SimpleWorkflowFeature], + workflow_label: str | NotGiven = NOT_GIVEN, + is_default: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + is_white_label_enabled: bool | NotGiven = NOT_GIVEN, + is_desktop_allowed: bool | NotGiven = NOT_GIVEN, + max_retry_attempts: int | NotGiven = NOT_GIVEN, + retry_window_days: int | None | NotGiven = NOT_GIVEN, + face_liveness_max_attempts: int | NotGiven = NOT_GIVEN, + face_match_max_attempts: int | NotGiven = NOT_GIVEN, + session_expiration_time: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.dict[str, object]: + """Create workflow. + + Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` - any other key (including `workflow_type`) is rejected with a 400. Not idempotent - each call creates a new workflow. Maximum 50 workflows per application. + + ``POST /v3/workflows/`` + + Args: + features: Verification features in execution order. The API links each item to the next one and adds one final status node automatically. + workflow_label: Display name for the workflow (max 50 characters). Defaults to `"ID Verification"` when omitted - always set it so workflows stay distinguishable. + is_default: Set this workflow as the default for new sessions. + status: Omit this field to create a published workflow ready for sessions. Use `draft` only when you want to save without publishing. + is_white_label_enabled: Enable white-label customization for sessions created with this workflow. + is_desktop_allowed: Allow the verification flow to run on desktop browsers. + max_retry_attempts: Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline. + retry_window_days: Rolling window in days used to count retries. `null` enforces an all-time limit. + face_liveness_max_attempts: Maximum liveness submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it. + face_match_max_attempts: Maximum face-match submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it. + session_expiration_time: Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.dict[str, object]", + self._client.request( + "post", + "/v3/workflows/", + cast_to=builtins.dict[str, object], + auth="api_key", + body={ + "features": features, + "workflow_label": workflow_label, + "is_default": is_default, + "status": status, + "is_white_label_enabled": is_white_label_enabled, + "is_desktop_allowed": is_desktop_allowed, + "max_retry_attempts": max_retry_attempts, + "retry_window_days": retry_window_days, + "face_liveness_max_attempts": face_liveness_max_attempts, + "face_match_max_attempts": face_match_max_attempts, + "session_expiration_time": session_expiration_time, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def retrieve( + self, + settings_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Get workflow. + + Fetch full configuration for one workflow version by `settings_uuid`: feature toggles, thresholds, allowed documents, graph, retry policy, callbacks, and pricing. + + ``GET /v3/workflows/{settings_uuid}/`` + + Args: + settings_uuid: Per-version UUID of the workflow (the `uuid` value returned by the list endpoint). Each version of a workflow has its own `settings_uuid`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + self._client.request( + "get", + "/v3/workflows/{settings_uuid}/", + cast_to=object, + path_params={"settings_uuid": settings_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def delete( + self, + settings_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete workflow. + + Permanently delete one workflow version (no undo). Existing sessions keep their verification results but their workflow reference is cleared; other versions in the same `workflow_id` group are not deleted. + + ``DELETE /v3/workflows/{settings_uuid}/`` + + Args: + settings_uuid: Per-version UUID of the workflow to delete. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + self._client.request( + "delete", + "/v3/workflows/{settings_uuid}/", + cast_to=None, + path_params={"settings_uuid": settings_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + def update( + self, + settings_uuid: str, + *, + workflow_label: str | NotGiven = NOT_GIVEN, + is_default: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + features: Sequence[SimpleWorkflowFeature] | NotGiven = NOT_GIVEN, + is_white_label_enabled: bool | NotGiven = NOT_GIVEN, + is_desktop_allowed: bool | NotGiven = NOT_GIVEN, + max_retry_attempts: int | NotGiven = NOT_GIVEN, + retry_window_days: int | None | NotGiven = NOT_GIVEN, + face_liveness_max_attempts: int | NotGiven = NOT_GIVEN, + face_match_max_attempts: int | NotGiven = NOT_GIVEN, + session_expiration_time: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.dict[str, object]: + """Update workflow. + + Update a workflow. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `uuid`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: "published"` on a **published** version → a NEW published version is created under the same `workflow_id` (new `uuid`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: "draft"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`"Cannot revert a published version to draft."`). The body accepts the same strict field whitelist as create - unknown keys are rejected with 400. + + ``PATCH /v3/workflows/{settings_uuid}/`` + + Args: + settings_uuid: Per-version UUID of the workflow to update. + workflow_label: Display name for the workflow (max 50 characters). + is_default: Set this workflow as the default for new sessions. + status: Controls versioning. Omit to update the current version in place (drafts auto-publish). `published` on an already-published version creates and returns a NEW version. `draft` is only valid while the version is still a draft. + features: If provided, this replaces the full feature order and configuration. + is_white_label_enabled: Enable white-label customization for sessions created with this workflow. + is_desktop_allowed: Allow the verification flow to run on desktop browsers. + max_retry_attempts: Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline. + retry_window_days: Rolling window in days used to count retries. `null` enforces an all-time limit. + face_liveness_max_attempts: Maximum liveness submissions per session. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it. + face_match_max_attempts: Maximum face-match submissions per session. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it. + session_expiration_time: Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.dict[str, object]", + self._client.request( + "patch", + "/v3/workflows/{settings_uuid}/", + cast_to=builtins.dict[str, object], + path_params={"settings_uuid": settings_uuid}, + auth="api_key", + body={ + "workflow_label": workflow_label, + "is_default": is_default, + "status": status, + "features": features, + "is_white_label_enabled": is_white_label_enabled, + "is_desktop_allowed": is_desktop_allowed, + "max_retry_attempts": max_retry_attempts, + "retry_window_days": retry_window_days, + "face_liveness_max_attempts": face_liveness_max_attempts, + "face_match_max_attempts": face_match_max_attempts, + "session_expiration_time": session_expiration_time, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class AsyncWorkflows(AsyncAPIResource): + """The ``workflows`` resource of the Didit API. + + Methods: ``create``, ``delete``, ``list``, ``retrieve``, ``update``. + """ + + @cached_property + def with_raw_response(self) -> AsyncWorkflowsWithRawResponse: + """Every method of this resource, returning the raw HTTP response.""" + return AsyncWorkflowsWithRawResponse(self) + + @cached_property + def with_streaming_response(self) -> AsyncWorkflowsWithStreamingResponse: + """Every method of this resource, returning the HTTP response before reading its body.""" + return AsyncWorkflowsWithStreamingResponse(self) + + def list( + self, + *, + limit: int | NotGiven = NOT_GIVEN, + offset: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> AsyncPaginator[WorkflowListItem]: + """List workflows. + + List your workflows, newest first, in a limit/offset pagination envelope (`{count, next, previous, results}`, default page size 50). Each row is one version; `workflow_id` is the stable group ID and new sessions use the latest `published` version. + + ``GET /v3/workflows/`` + + Args: + limit: Page size (number of workflows per page). + offset: Number of rows to skip before the first returned workflow. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "AsyncPaginator[WorkflowListItem]", + self._client.request_page( + "get", + "/v3/workflows/", + cast_to=WorkflowsListResponse, + item_type=WorkflowListItem, + auth="api_key", + query={"limit": limit, "offset": offset}, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def create( + self, + *, + features: Sequence[SimpleWorkflowFeature], + workflow_label: str | NotGiven = NOT_GIVEN, + is_default: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + is_white_label_enabled: bool | NotGiven = NOT_GIVEN, + is_desktop_allowed: bool | NotGiven = NOT_GIVEN, + max_retry_attempts: int | NotGiven = NOT_GIVEN, + retry_window_days: int | None | NotGiven = NOT_GIVEN, + face_liveness_max_attempts: int | NotGiven = NOT_GIVEN, + face_match_max_attempts: int | NotGiven = NOT_GIVEN, + session_expiration_time: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.dict[str, object]: + """Create workflow. + + Create a simple workflow. Send `features` in execution order (ordering rules apply); defaults to `published` v1. The body accepts ONLY the fields listed in `SimpleWorkflowRequest` - any other key (including `workflow_type`) is rejected with a 400. Not idempotent - each call creates a new workflow. Maximum 50 workflows per application. + + ``POST /v3/workflows/`` + + Args: + features: Verification features in execution order. The API links each item to the next one and adds one final status node automatically. + workflow_label: Display name for the workflow (max 50 characters). Defaults to `"ID Verification"` when omitted - always set it so workflows stay distinguishable. + is_default: Set this workflow as the default for new sessions. + status: Omit this field to create a published workflow ready for sessions. Use `draft` only when you want to save without publishing. + is_white_label_enabled: Enable white-label customization for sessions created with this workflow. + is_desktop_allowed: Allow the verification flow to run on desktop browsers. + max_retry_attempts: Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline. + retry_window_days: Rolling window in days used to count retries. `null` enforces an all-time limit. + face_liveness_max_attempts: Maximum liveness submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it. + face_match_max_attempts: Maximum face-match submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it. + session_expiration_time: Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.dict[str, object]", + await self._client.request( + "post", + "/v3/workflows/", + cast_to=builtins.dict[str, object], + auth="api_key", + body={ + "features": features, + "workflow_label": workflow_label, + "is_default": is_default, + "status": status, + "is_white_label_enabled": is_white_label_enabled, + "is_desktop_allowed": is_desktop_allowed, + "max_retry_attempts": max_retry_attempts, + "retry_window_days": retry_window_days, + "face_liveness_max_attempts": face_liveness_max_attempts, + "face_match_max_attempts": face_match_max_attempts, + "session_expiration_time": session_expiration_time, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def retrieve( + self, + settings_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> object: + """Get workflow. + + Fetch full configuration for one workflow version by `settings_uuid`: feature toggles, thresholds, allowed documents, graph, retry policy, callbacks, and pricing. + + ``GET /v3/workflows/{settings_uuid}/`` + + Args: + settings_uuid: Per-version UUID of the workflow (the `uuid` value returned by the list endpoint). Each version of a workflow has its own `settings_uuid`. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "object", + await self._client.request( + "get", + "/v3/workflows/{settings_uuid}/", + cast_to=object, + path_params={"settings_uuid": settings_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def delete( + self, + settings_uuid: str, + *, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> None: + """Delete workflow. + + Permanently delete one workflow version (no undo). Existing sessions keep their verification results but their workflow reference is cleared; other versions in the same `workflow_id` group are not deleted. + + ``DELETE /v3/workflows/{settings_uuid}/`` + + Args: + settings_uuid: Per-version UUID of the workflow to delete. + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "None", + await self._client.request( + "delete", + "/v3/workflows/{settings_uuid}/", + cast_to=None, + path_params={"settings_uuid": settings_uuid}, + auth="api_key", + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + async def update( + self, + settings_uuid: str, + *, + workflow_label: str | NotGiven = NOT_GIVEN, + is_default: bool | NotGiven = NOT_GIVEN, + status: Literal["draft", "published"] | str | NotGiven = NOT_GIVEN, + features: Sequence[SimpleWorkflowFeature] | NotGiven = NOT_GIVEN, + is_white_label_enabled: bool | NotGiven = NOT_GIVEN, + is_desktop_allowed: bool | NotGiven = NOT_GIVEN, + max_retry_attempts: int | NotGiven = NOT_GIVEN, + retry_window_days: int | None | NotGiven = NOT_GIVEN, + face_liveness_max_attempts: int | NotGiven = NOT_GIVEN, + face_match_max_attempts: int | NotGiven = NOT_GIVEN, + session_expiration_time: int | NotGiven = NOT_GIVEN, + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN, + ) -> builtins.dict[str, object]: + """Update workflow. + + Update a workflow. Versioning depends on the `status` field you send and the current state of the version: (1) no `status` on a **published** version → the published version is updated in place (same `uuid`, same `version`); (2) no `status` on a **draft** → the draft is updated and immediately published; (3) `status: "published"` on a **published** version → a NEW published version is created under the same `workflow_id` (new `uuid`, `version` + 1) and returned, and any existing draft in the group is deleted; (4) `status: "draft"` keeps a draft as a draft (autosave) but is rejected with 400 on a published version (`"Cannot revert a published version to draft."`). The body accepts the same strict field whitelist as create - unknown keys are rejected with 400. + + ``PATCH /v3/workflows/{settings_uuid}/`` + + Args: + settings_uuid: Per-version UUID of the workflow to update. + workflow_label: Display name for the workflow (max 50 characters). + is_default: Set this workflow as the default for new sessions. + status: Controls versioning. Omit to update the current version in place (drafts auto-publish). `published` on an already-published version creates and returns a NEW version. `draft` is only valid while the version is still a draft. + features: If provided, this replaces the full feature order and configuration. + is_white_label_enabled: Enable white-label customization for sessions created with this workflow. + is_desktop_allowed: Allow the verification flow to run on desktop browsers. + max_retry_attempts: Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline. + retry_window_days: Rolling window in days used to count retries. `null` enforces an all-time limit. + face_liveness_max_attempts: Maximum liveness submissions per session. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it. + face_match_max_attempts: Maximum face-match submissions per session. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it. + session_expiration_time: Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200). + extra_headers: Headers to add to (or, with ``None``, remove from) this request. + extra_query: Query parameters to add to this request. + extra_body: Top-level JSON fields to add to this request's body. + timeout: Overrides the client's timeout for this request, in seconds. + """ + return cast( + "builtins.dict[str, object]", + await self._client.request( + "patch", + "/v3/workflows/{settings_uuid}/", + cast_to=builtins.dict[str, object], + path_params={"settings_uuid": settings_uuid}, + auth="api_key", + body={ + "workflow_label": workflow_label, + "is_default": is_default, + "status": status, + "features": features, + "is_white_label_enabled": is_white_label_enabled, + "is_desktop_allowed": is_desktop_allowed, + "max_retry_attempts": max_retry_attempts, + "retry_window_days": retry_window_days, + "face_liveness_max_attempts": face_liveness_max_attempts, + "face_match_max_attempts": face_match_max_attempts, + "session_expiration_time": session_expiration_time, + }, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + ), + ) + + +class WorkflowsWithRawResponse: + def __init__(self, resource: Workflows) -> None: + self._resource = resource + self.list = to_raw_response_wrapper(resource.list) + self.create = to_raw_response_wrapper(resource.create) + self.retrieve = to_raw_response_wrapper(resource.retrieve) + self.delete = to_raw_response_wrapper(resource.delete) + self.update = to_raw_response_wrapper(resource.update) + + +class AsyncWorkflowsWithRawResponse: + def __init__(self, resource: AsyncWorkflows) -> None: + self._resource = resource + self.list = async_to_raw_response_wrapper(resource.list) + self.create = async_to_raw_response_wrapper(resource.create) + self.retrieve = async_to_raw_response_wrapper(resource.retrieve) + self.delete = async_to_raw_response_wrapper(resource.delete) + self.update = async_to_raw_response_wrapper(resource.update) + + +class WorkflowsWithStreamingResponse: + def __init__(self, resource: Workflows) -> None: + self._resource = resource + self.list = to_streamed_response_wrapper(resource.list) + self.create = to_streamed_response_wrapper(resource.create) + self.retrieve = to_streamed_response_wrapper(resource.retrieve) + self.delete = to_streamed_response_wrapper(resource.delete) + self.update = to_streamed_response_wrapper(resource.update) + + +class AsyncWorkflowsWithStreamingResponse: + def __init__(self, resource: AsyncWorkflows) -> None: + self._resource = resource + self.list = async_to_streamed_response_wrapper(resource.list) + self.create = async_to_streamed_response_wrapper(resource.create) + self.retrieve = async_to_streamed_response_wrapper(resource.retrieve) + self.delete = async_to_streamed_response_wrapper(resource.delete) + self.update = async_to_streamed_response_wrapper(resource.update) diff --git a/src/didit/types/__init__.py b/src/didit/types/__init__.py new file mode 100644 index 0000000..49c3297 --- /dev/null +++ b/src/didit/types/__init__.py @@ -0,0 +1,4072 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Every request and response type of the Didit API. + +Names follow docs/DESIGN.md section 5.1. Modules load on first use, so +``import didit`` stays fast. +""" + +from __future__ import annotations + +import importlib +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from didit.types.account.applications.create_params import ( + AccountApplicationsCreateParams as AccountApplicationsCreateParams, + ) + from didit.types.account.applications.update_params import ( + AccountApplicationsUpdateParams as AccountApplicationsUpdateParams, + ) + from didit.types.account.login_params import AccountLoginParams as AccountLoginParams + from didit.types.account.register_params import AccountRegisterParams as AccountRegisterParams + from didit.types.account.resend_otp_params import ( + AccountResendOtpParams as AccountResendOtpParams, + ) + from didit.types.account.verify_email_params import ( + AccountVerifyEmailParams as AccountVerifyEmailParams, + ) + from didit.types.age_estimation.estimate_params import ( + AgeEstimationEstimateParams as AgeEstimationEstimateParams, + ) + from didit.types.age_estimation.estimate_response import ( + AgeEstimationEstimateResponse as AgeEstimationEstimateResponse, + ) + from didit.types.age_estimation.estimate_response import ( + AgeEstimationEstimateResponseAgeEstimation as AgeEstimationEstimateResponseAgeEstimation, + ) + from didit.types.age_estimation.estimate_response import ( + AgeEstimationEstimateResponseAgeEstimationUserImage as AgeEstimationEstimateResponseAgeEstimationUserImage, + ) + from didit.types.age_estimation.estimate_response import ( + AgeEstimationEstimateResponseAgeEstimationUserImageEntities as AgeEstimationEstimateResponseAgeEstimationUserImageEntities, + ) + from didit.types.age_estimation.estimate_response import ( + AgeEstimationEstimateResponseAgeEstimationWarnings as AgeEstimationEstimateResponseAgeEstimationWarnings, + ) + from didit.types.aml.screen_params import AmlScreenParams as AmlScreenParams + from didit.types.aml.screen_response import AmlScreenResponse as AmlScreenResponse + from didit.types.aml.screen_response import AmlScreenResponseAml as AmlScreenResponseAml + from didit.types.aml.screen_response import AmlScreenResponseAmlHits as AmlScreenResponseAmlHits + from didit.types.aml.screen_response import ( + AmlScreenResponseAmlHitsAdverseMediaMatches as AmlScreenResponseAmlHitsAdverseMediaMatches, + ) + from didit.types.aml.screen_response import ( + AmlScreenResponseAmlHitsLinkedEntities as AmlScreenResponseAmlHitsLinkedEntities, + ) + from didit.types.aml.screen_response import ( + AmlScreenResponseAmlHitsPepMatches as AmlScreenResponseAmlHitsPepMatches, + ) + from didit.types.aml.screen_response import ( + AmlScreenResponseAmlHitsScoreBreakdown as AmlScreenResponseAmlHitsScoreBreakdown, + ) + from didit.types.aml.screen_response import ( + AmlScreenResponseAmlScreenedData as AmlScreenResponseAmlScreenedData, + ) + from didit.types.aml.screen_response import ( + AmlScreenResponseAmlWarnings as AmlScreenResponseAmlWarnings, + ) + from didit.types.aml_monitoring.retrieve_job_response import ( + AmlMonitoringRetrieveJobResponse as AmlMonitoringRetrieveJobResponse, + ) + from didit.types.aml_monitoring.retrieve_job_response import ( + AmlMonitoringRetrieveJobResponseOutcomes as AmlMonitoringRetrieveJobResponseOutcomes, + ) + from didit.types.aml_monitoring.retrieve_job_response import ( + AmlMonitoringRetrieveJobResponseResults as AmlMonitoringRetrieveJobResponseResults, + ) + from didit.types.billing.retrieve_balance_response import ( + BillingRetrieveBalanceResponse as BillingRetrieveBalanceResponse, + ) + from didit.types.billing.top_up_params import BillingTopUpParams as BillingTopUpParams + from didit.types.billing.top_up_response import BillingTopUpResponse as BillingTopUpResponse + from didit.types.biometric_templates.batch_delete_params import ( + BiometricTemplatesBatchDeleteParams as BiometricTemplatesBatchDeleteParams, + ) + from didit.types.biometric_templates.batch_delete_response import ( + BiometricTemplatesBatchDeleteResponse as BiometricTemplatesBatchDeleteResponse, + ) + from didit.types.biometric_templates.batch_delete_response import ( + BiometricTemplatesBatchDeleteResponseResults as BiometricTemplatesBatchDeleteResponseResults, + ) + from didit.types.biometric_templates.count_response import ( + BiometricTemplatesCountResponse as BiometricTemplatesCountResponse, + ) + from didit.types.biometric_templates.delete_response import ( + BiometricTemplatesDeleteResponse as BiometricTemplatesDeleteResponse, + ) + from didit.types.biometric_templates.list_response import ( + BiometricTemplatesListResponse as BiometricTemplatesListResponse, + ) + from didit.types.biometric_templates.list_response import ( + BiometricTemplatesListResponseResults as BiometricTemplatesListResponseResults, + ) + from didit.types.biometric_templates.retrieve_response import ( + BiometricTemplatesRetrieveResponse as BiometricTemplatesRetrieveResponse, + ) + from didit.types.business_sessions.list_networks_response import ( + BusinessSessionsListNetworksResponse as BusinessSessionsListNetworksResponse, + ) + from didit.types.businesses.batch_delete_params import ( + BusinessesBatchDeleteParams as BusinessesBatchDeleteParams, + ) + from didit.types.businesses.batch_delete_response import ( + BusinessesBatchDeleteResponse as BusinessesBatchDeleteResponse, + ) + from didit.types.businesses.create_params import ( + BusinessesCreateParams as BusinessesCreateParams, + ) + from didit.types.businesses.list_networks_response import ( + BusinessesListNetworksResponse as BusinessesListNetworksResponse, + ) + from didit.types.businesses.list_response import ( + BusinessesListResponse as BusinessesListResponse, + ) + from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( + BusinessesRetrieveOngoingMonitoringJobResponse as BusinessesRetrieveOngoingMonitoringJobResponse, + ) + from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( + BusinessesRetrieveOngoingMonitoringJobResponseEstimate as BusinessesRetrieveOngoingMonitoringJobResponseEstimate, + ) + from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( + BusinessesRetrieveOngoingMonitoringJobResponseOutcomes as BusinessesRetrieveOngoingMonitoringJobResponseOutcomes, + ) + from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( + BusinessesRetrieveSessionOngoingMonitoringJobResponse as BusinessesRetrieveSessionOngoingMonitoringJobResponse, + ) + from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( + BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate as BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate, + ) + from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( + BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes as BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes, + ) + from didit.types.businesses.set_aml_monitoring_params import ( + BusinessesSetAmlMonitoringParams as BusinessesSetAmlMonitoringParams, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponse202 as BusinessesSetAmlMonitoringResponse202, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponse202Estimate as BusinessesSetAmlMonitoringResponse202Estimate, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponse202Outcomes as BusinessesSetAmlMonitoringResponse202Outcomes, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponse202Results as BusinessesSetAmlMonitoringResponse202Results, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponseVariant1 as BusinessesSetAmlMonitoringResponseVariant1, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponseVariant2 as BusinessesSetAmlMonitoringResponseVariant2, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponseVariant2Estimate as BusinessesSetAmlMonitoringResponseVariant2Estimate, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponseVariant2Outcomes as BusinessesSetAmlMonitoringResponseVariant2Outcomes, + ) + from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponseVariant2Results as BusinessesSetAmlMonitoringResponseVariant2Results, + ) + from didit.types.businesses.set_ongoing_monitoring_params import ( + BusinessesSetOngoingMonitoringParams as BusinessesSetOngoingMonitoringParams, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponse202 as BusinessesSetOngoingMonitoringResponse202, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponse202Estimate as BusinessesSetOngoingMonitoringResponse202Estimate, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponse202Outcomes as BusinessesSetOngoingMonitoringResponse202Outcomes, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponseVariant1 as BusinessesSetOngoingMonitoringResponseVariant1, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponseVariant2 as BusinessesSetOngoingMonitoringResponseVariant2, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponseVariant2Estimate as BusinessesSetOngoingMonitoringResponseVariant2Estimate, + ) + from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponseVariant2Outcomes as BusinessesSetOngoingMonitoringResponseVariant2Outcomes, + ) + from didit.types.businesses.set_session_ongoing_monitoring_params import ( + BusinessesSetSessionOngoingMonitoringParams as BusinessesSetSessionOngoingMonitoringParams, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponse202 as BusinessesSetSessionOngoingMonitoringResponse202, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponse202Estimate as BusinessesSetSessionOngoingMonitoringResponse202Estimate, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponse202Outcomes as BusinessesSetSessionOngoingMonitoringResponse202Outcomes, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponseVariant1 as BusinessesSetSessionOngoingMonitoringResponseVariant1, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponseVariant2 as BusinessesSetSessionOngoingMonitoringResponseVariant2, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate as BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate, + ) + from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes as BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes, + ) + from didit.types.businesses.update_params import ( + BusinessesUpdateParams as BusinessesUpdateParams, + ) + from didit.types.businesses.update_status_params import ( + BusinessesUpdateStatusParams as BusinessesUpdateStatusParams, + ) + from didit.types.cases.apply_entity_action_params import ( + CasesApplyEntityActionParams as CasesApplyEntityActionParams, + ) + from didit.types.cases.assign_params import CasesAssignParams as CasesAssignParams + from didit.types.cases.blueprints.create_params import ( + CasesBlueprintsCreateParams as CasesBlueprintsCreateParams, + ) + from didit.types.cases.blueprints.discard_draft_params import ( + CasesBlueprintsDiscardDraftParams as CasesBlueprintsDiscardDraftParams, + ) + from didit.types.cases.blueprints.install_presets_params import ( + CasesBlueprintsInstallPresetsParams as CasesBlueprintsInstallPresetsParams, + ) + from didit.types.cases.blueprints.install_presets_response import ( + CasesBlueprintsInstallPresetsResponse as CasesBlueprintsInstallPresetsResponse, + ) + from didit.types.cases.blueprints.list_presets_response import ( + CasesBlueprintsListPresetsResponse as CasesBlueprintsListPresetsResponse, + ) + from didit.types.cases.blueprints.list_presets_response import ( + CasesBlueprintsListPresetsResponseResults as CasesBlueprintsListPresetsResponseResults, + ) + from didit.types.cases.blueprints.list_response import ( + CasesBlueprintsListResponse as CasesBlueprintsListResponse, + ) + from didit.types.cases.blueprints.publish_params import ( + CasesBlueprintsPublishParams as CasesBlueprintsPublishParams, + ) + from didit.types.cases.blueprints.save_draft_params import ( + CasesBlueprintsSaveDraftParams as CasesBlueprintsSaveDraftParams, + ) + from didit.types.cases.blueprints.update_params import ( + CasesBlueprintsUpdateParams as CasesBlueprintsUpdateParams, + ) + from didit.types.cases.bulk_assign_params import CasesBulkAssignParams as CasesBulkAssignParams + from didit.types.cases.bulk_assign_response import ( + CasesBulkAssignResponse as CasesBulkAssignResponse, + ) + from didit.types.cases.bulk_assign_response import ( + CasesBulkAssignResponseFailures as CasesBulkAssignResponseFailures, + ) + from didit.types.cases.bulk_update_status_params import ( + CasesBulkUpdateStatusParams as CasesBulkUpdateStatusParams, + ) + from didit.types.cases.bulk_update_status_response import ( + CasesBulkUpdateStatusResponse as CasesBulkUpdateStatusResponse, + ) + from didit.types.cases.bulk_update_status_response import ( + CasesBulkUpdateStatusResponseFailures as CasesBulkUpdateStatusResponseFailures, + ) + from didit.types.cases.checklist.toggle_item_params import ( + CasesChecklistToggleItemParams as CasesChecklistToggleItemParams, + ) + from didit.types.cases.create_params import CasesCreateParams as CasesCreateParams + from didit.types.cases.create_params import CasesCreateParamsSubject as CasesCreateParamsSubject + from didit.types.cases.find_duplicates_params import ( + CasesFindDuplicatesParams as CasesFindDuplicatesParams, + ) + from didit.types.cases.links.create_params import ( + CasesLinksCreateParams as CasesLinksCreateParams, + ) + from didit.types.cases.list_events_response import ( + CasesListEventsResponse as CasesListEventsResponse, + ) + from didit.types.cases.list_response import CasesListResponse as CasesListResponse + from didit.types.cases.list_reviewers_response import ( + CasesListReviewersResponse as CasesListReviewersResponse, + ) + from didit.types.cases.list_reviewers_response import ( + CasesListReviewersResponseResults as CasesListReviewersResponseResults, + ) + from didit.types.cases.notes.create_attachment_upload_params import ( + CasesNotesCreateAttachmentUploadParams as CasesNotesCreateAttachmentUploadParams, + ) + from didit.types.cases.notes.create_attachment_upload_response import ( + CasesNotesCreateAttachmentUploadResponse as CasesNotesCreateAttachmentUploadResponse, + ) + from didit.types.cases.notes.create_params import ( + CasesNotesCreateParams as CasesNotesCreateParams, + ) + from didit.types.cases.notes.retrieve_attachment_url_response import ( + CasesNotesRetrieveAttachmentUrlResponse as CasesNotesRetrieveAttachmentUrlResponse, + ) + from didit.types.cases.notes.update_params import ( + CasesNotesUpdateParams as CasesNotesUpdateParams, + ) + from didit.types.cases.print_response import CasesPrintResponse as CasesPrintResponse + from didit.types.cases.regulatory_reports.create_params import ( + CasesRegulatoryReportsCreateParams as CasesRegulatoryReportsCreateParams, + ) + from didit.types.cases.regulatory_reports.create_params import ( + CasesRegulatoryReportsCreateParamsNarrative as CasesRegulatoryReportsCreateParamsNarrative, + ) + from didit.types.cases.regulatory_reports.list_response import ( + CasesRegulatoryReportsListResponse as CasesRegulatoryReportsListResponse, + ) + from didit.types.cases.reject_resolution_params import ( + CasesRejectResolutionParams as CasesRejectResolutionParams, + ) + from didit.types.cases.requests.create_params import ( + CasesRequestsCreateParams as CasesRequestsCreateParams, + ) + from didit.types.cases.requests.preview_params import ( + CasesRequestsPreviewParams as CasesRequestsPreviewParams, + ) + from didit.types.cases.resolve_params import CasesResolveParams as CasesResolveParams + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponse as CasesRetrieveStatisticsResponse, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseQueue as CasesRetrieveStatisticsResponseQueue, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseSeries as CasesRetrieveStatisticsResponseSeries, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseTables as CasesRetrieveStatisticsResponseTables, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseTablesBlueprint as CasesRetrieveStatisticsResponseTablesBlueprint, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseTablesOfficer as CasesRetrieveStatisticsResponseTablesOfficer, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseTablesSource as CasesRetrieveStatisticsResponseTablesSource, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseTablesStatus as CasesRetrieveStatisticsResponseTablesStatus, + ) + from didit.types.cases.retrieve_statistics_response import ( + CasesRetrieveStatisticsResponseTotals as CasesRetrieveStatisticsResponseTotals, + ) + from didit.types.cases.routing_rules.create_params import ( + CasesRoutingRulesCreateParams as CasesRoutingRulesCreateParams, + ) + from didit.types.cases.routing_rules.discard_draft_params import ( + CasesRoutingRulesDiscardDraftParams as CasesRoutingRulesDiscardDraftParams, + ) + from didit.types.cases.routing_rules.list_response import ( + CasesRoutingRulesListResponse as CasesRoutingRulesListResponse, + ) + from didit.types.cases.routing_rules.preview_params import ( + CasesRoutingRulesPreviewParams as CasesRoutingRulesPreviewParams, + ) + from didit.types.cases.routing_rules.publish_params import ( + CasesRoutingRulesPublishParams as CasesRoutingRulesPublishParams, + ) + from didit.types.cases.routing_rules.save_draft_params import ( + CasesRoutingRulesSaveDraftParams as CasesRoutingRulesSaveDraftParams, + ) + from didit.types.cases.routing_rules.update_params import ( + CasesRoutingRulesUpdateParams as CasesRoutingRulesUpdateParams, + ) + from didit.types.cases.teams.create_params import ( + CasesTeamsCreateParams as CasesTeamsCreateParams, + ) + from didit.types.cases.teams.list_response import ( + CasesTeamsListResponse as CasesTeamsListResponse, + ) + from didit.types.cases.teams.update_params import ( + CasesTeamsUpdateParams as CasesTeamsUpdateParams, + ) + from didit.types.cases.transfer_params import CasesTransferParams as CasesTransferParams + from didit.types.cases.update_params import CasesUpdateParams as CasesUpdateParams + from didit.types.components.application import Application as Application + from didit.types.components.application_credentials_summary import ( + ApplicationCredentialsSummary as ApplicationCredentialsSummary, + ) + from didit.types.components.branding_customization import ( + BrandingCustomization as BrandingCustomization, + ) + from didit.types.components.business_detail_item import BusinessDetailItem as BusinessDetailItem + from didit.types.components.business_list_item import BusinessListItem as BusinessListItem + from didit.types.components.business_list_item import ( + BusinessListItemFeaturesList as BusinessListItemFeaturesList, + ) + from didit.types.components.business_list_item import ( + BusinessListItemTags as BusinessListItemTags, + ) + from didit.types.components.case_action_action_enum import ( + CaseActionActionEnum as CaseActionActionEnum, + ) + from didit.types.components.case_action_case_action_receipt import ( + CaseActionCaseActionReceipt as CaseActionCaseActionReceipt, + ) + from didit.types.components.case_action_case_action_receipt_page import ( + CaseActionCaseActionReceiptPage as CaseActionCaseActionReceiptPage, + ) + from didit.types.components.case_action_case_entity_action_result import ( + CaseActionCaseEntityActionResult as CaseActionCaseEntityActionResult, + ) + from didit.types.components.case_action_entity_type_enum import ( + CaseActionEntityTypeEnum as CaseActionEntityTypeEnum, + ) + from didit.types.components.case_action_status_enum import ( + CaseActionStatusEnum as CaseActionStatusEnum, + ) + from didit.types.components.case_assignee import CaseAssignee as CaseAssignee + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetail as CaseBlueprintDetail, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfig as CaseBlueprintDetailContentConfig, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigAmlControl as CaseBlueprintDetailContentConfigAmlControl, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigChecklist as CaseBlueprintDetailContentConfigChecklist, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigChecklistItems as CaseBlueprintDetailContentConfigChecklistItems, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigFinancial as CaseBlueprintDetailContentConfigFinancial, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigIdentity as CaseBlueprintDetailContentConfigIdentity, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigReporting as CaseBlueprintDetailContentConfigReporting, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailContentConfigVerifications as CaseBlueprintDetailContentConfigVerifications, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailCreationSources as CaseBlueprintDetailCreationSources, + ) + from didit.types.components.case_blueprint_detail import ( + CaseBlueprintDetailReceivesEscalationFrom as CaseBlueprintDetailReceivesEscalationFrom, + ) + from didit.types.components.case_blueprint_list_item import ( + CaseBlueprintListItem as CaseBlueprintListItem, + ) + from didit.types.components.case_blueprint_summary import ( + CaseBlueprintSummary as CaseBlueprintSummary, + ) + from didit.types.components.case_checklist_item import CaseChecklistItem as CaseChecklistItem + from didit.types.components.case_collaboration_case_note import ( + CaseCollaborationCaseNote as CaseCollaborationCaseNote, + ) + from didit.types.components.case_collaboration_case_note_reply import ( + CaseCollaborationCaseNoteReply as CaseCollaborationCaseNoteReply, + ) + from didit.types.components.case_collaboration_paginated_case_note_list import ( + CaseCollaborationPaginatedCaseNoteList as CaseCollaborationPaginatedCaseNoteList, + ) + from didit.types.components.case_content_config import CaseContentConfig as CaseContentConfig + from didit.types.components.case_content_config import ( + CaseContentConfigAmlControl as CaseContentConfigAmlControl, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigAmlControlParam as CaseContentConfigAmlControlParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigChecklist as CaseContentConfigChecklist, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigChecklistItems as CaseContentConfigChecklistItems, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigChecklistItemsParam as CaseContentConfigChecklistItemsParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigChecklistParam as CaseContentConfigChecklistParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigFinancial as CaseContentConfigFinancial, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigFinancialParam as CaseContentConfigFinancialParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigIdentity as CaseContentConfigIdentity, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigIdentityParam as CaseContentConfigIdentityParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigParam as CaseContentConfigParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigReporting as CaseContentConfigReporting, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigReportingParam as CaseContentConfigReportingParam, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigVerifications as CaseContentConfigVerifications, + ) + from didit.types.components.case_content_config import ( + CaseContentConfigVerificationsParam as CaseContentConfigVerificationsParam, + ) + from didit.types.components.case_detail import CaseDetail as CaseDetail + from didit.types.components.case_detail import ( + CaseDetailPendingResolution as CaseDetailPendingResolution, + ) + from didit.types.components.case_duplicate_case_duplicate import ( + CaseDuplicateCaseDuplicate as CaseDuplicateCaseDuplicate, + ) + from didit.types.components.case_duplicate_case_duplicate_page import ( + CaseDuplicateCaseDuplicatePage as CaseDuplicateCaseDuplicatePage, + ) + from didit.types.components.case_duplicate_case_fraud_source_context_request import ( + CaseDuplicateCaseFraudSourceContextRequest as CaseDuplicateCaseFraudSourceContextRequest, + ) + from didit.types.components.case_duplicate_case_link_create_request import ( + CaseDuplicateCaseLinkCreateRequest as CaseDuplicateCaseLinkCreateRequest, + ) + from didit.types.components.case_duplicate_case_subject_input_request import ( + CaseDuplicateCaseSubjectInputRequest as CaseDuplicateCaseSubjectInputRequest, + ) + from didit.types.components.case_duplicate_entity_type_enum import ( + CaseDuplicateEntityTypeEnum as CaseDuplicateEntityTypeEnum, + ) + from didit.types.components.case_duplicate_priority_enum import ( + CaseDuplicatePriorityEnum as CaseDuplicatePriorityEnum, + ) + from didit.types.components.case_duplicate_status_enum import ( + CaseDuplicateStatusEnum as CaseDuplicateStatusEnum, + ) + from didit.types.components.case_duplicate_type_enum import ( + CaseDuplicateTypeEnum as CaseDuplicateTypeEnum, + ) + from didit.types.components.case_event import CaseEvent as CaseEvent + from didit.types.components.case_link import CaseLink as CaseLink + from didit.types.components.case_list_item import CaseListItem as CaseListItem + from didit.types.components.case_note import CaseNote as CaseNote + from didit.types.components.case_note import CaseNoteAttachments as CaseNoteAttachments + from didit.types.components.case_publication_assignment_method_enum import ( + CasePublicationAssignmentMethodEnum as CasePublicationAssignmentMethodEnum, + ) + from didit.types.components.case_publication_case_blueprint import ( + CasePublicationCaseBlueprint as CasePublicationCaseBlueprint, + ) + from didit.types.components.case_publication_paginated_policy_revision_list import ( + CasePublicationPaginatedPolicyRevisionList as CasePublicationPaginatedPolicyRevisionList, + ) + from didit.types.components.case_publication_policy_draft import ( + CasePublicationPolicyDraft as CasePublicationPolicyDraft, + ) + from didit.types.components.case_publication_policy_draft_response import ( + CasePublicationPolicyDraftResponse as CasePublicationPolicyDraftResponse, + ) + from didit.types.components.case_publication_policy_revision import ( + CasePublicationPolicyRevision as CasePublicationPolicyRevision, + ) + from didit.types.components.case_review_team import CaseReviewTeam as CaseReviewTeam + from didit.types.components.case_review_team_member import ( + CaseReviewTeamMember as CaseReviewTeamMember, + ) + from didit.types.components.case_review_team_member import ( + CaseReviewTeamMemberParam as CaseReviewTeamMemberParam, + ) + from didit.types.components.case_routing_condition import ( + CaseRoutingCondition as CaseRoutingCondition, + ) + from didit.types.components.case_routing_condition import ( + CaseRoutingConditionParam as CaseRoutingConditionParam, + ) + from didit.types.components.case_routing_preview import CaseRoutingPreview as CaseRoutingPreview + from didit.types.components.case_routing_preview import ( + CaseRoutingPreviewCandidates as CaseRoutingPreviewCandidates, + ) + from didit.types.components.case_routing_preview import ( + CaseRoutingPreviewMatchedRules as CaseRoutingPreviewMatchedRules, + ) + from didit.types.components.case_routing_rule import CaseRoutingRule as CaseRoutingRule + from didit.types.components.case_routing_rule_create import ( + CaseRoutingRuleCreate as CaseRoutingRuleCreate, + ) + from didit.types.components.case_subject import CaseSubject as CaseSubject + from didit.types.components.customer_messaging_case_request import ( + CustomerMessagingCaseRequest as CustomerMessagingCaseRequest, + ) + from didit.types.components.customer_messaging_case_request_contact import ( + CustomerMessagingCaseRequestContact as CustomerMessagingCaseRequestContact, + ) + from didit.types.components.customer_messaging_case_request_options import ( + CustomerMessagingCaseRequestOptions as CustomerMessagingCaseRequestOptions, + ) + from didit.types.components.customer_messaging_case_request_preview_response import ( + CustomerMessagingCaseRequestPreviewResponse as CustomerMessagingCaseRequestPreviewResponse, + ) + from didit.types.components.customer_messaging_language_enum import ( + CustomerMessagingLanguageEnum as CustomerMessagingLanguageEnum, + ) + from didit.types.components.customer_messaging_languages_enum import ( + CustomerMessagingLanguagesEnum as CustomerMessagingLanguagesEnum, + ) + from didit.types.components.customer_messaging_paginated_case_request_list import ( + CustomerMessagingPaginatedCaseRequestList as CustomerMessagingPaginatedCaseRequestList, + ) + from didit.types.components.customer_messaging_purpose_enum import ( + CustomerMessagingPurposeEnum as CustomerMessagingPurposeEnum, + ) + from didit.types.components.customer_messaging_purposes_enum import ( + CustomerMessagingPurposesEnum as CustomerMessagingPurposesEnum, + ) + from didit.types.components.digital_id_wallet_api_audkenni_wallet_verification_start_country_enum import ( + DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum as DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_connectid_wallet_verification_start_country_enum import ( + DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum as DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_country520_enum import ( + DigitalIdWalletApiCountry520Enum as DigitalIdWalletApiCountry520Enum, + ) + from didit.types.components.digital_id_wallet_api_country_afc_enum import ( + DigitalIdWalletApiCountryAfcEnum as DigitalIdWalletApiCountryAfcEnum, + ) + from didit.types.components.digital_id_wallet_api_country_e2d_enum import ( + DigitalIdWalletApiCountryE2dEnum as DigitalIdWalletApiCountryE2dEnum, + ) + from didit.types.components.digital_id_wallet_api_country_ee9_enum import ( + DigitalIdWalletApiCountryEe9Enum as DigitalIdWalletApiCountryEe9Enum, + ) + from didit.types.components.digital_id_wallet_api_device_enum import ( + DigitalIdWalletApiDeviceEnum as DigitalIdWalletApiDeviceEnum, + ) + from didit.types.components.digital_id_wallet_api_diia_wallet_verification_start_country_enum import ( + DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum as DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum import ( + DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum as DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_eudi_wallet_verification_start_country_enum import ( + DigitalIdWalletApiEudiWalletVerificationStartCountryEnum as DigitalIdWalletApiEudiWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum import ( + DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum as DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_ftn_wallet_verification_start_country_enum import ( + DigitalIdWalletApiFtnWalletVerificationStartCountryEnum as DigitalIdWalletApiFtnWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_govbr_wallet_verification_start_country_enum import ( + DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum as DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum import ( + DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum as DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_idin_wallet_verification_start_country_enum import ( + DigitalIdWalletApiIdinWalletVerificationStartCountryEnum as DigitalIdWalletApiIdinWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_interaction_enum import ( + DigitalIdWalletApiInteractionEnum as DigitalIdWalletApiInteractionEnum, + ) + from didit.types.components.digital_id_wallet_api_itsme_wallet_verification_start_country_enum import ( + DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum as DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_mitid_wallet_verification_start_country_enum import ( + DigitalIdWalletApiMitidWalletVerificationStartCountryEnum as DigitalIdWalletApiMitidWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum import ( + DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum as DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum import ( + DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum as DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_smart_id_wallet_verification_start_country_enum import ( + DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum as DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum import ( + DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum as DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum, + ) + from didit.types.components.digital_id_wallet_api_wallet import ( + DigitalIdWalletApiWallet as DigitalIdWalletApiWallet, + ) + from didit.types.components.digital_id_wallet_api_wallet_catalog_entry import ( + DigitalIdWalletApiWalletCatalogEntry as DigitalIdWalletApiWalletCatalogEntry, + ) + from didit.types.components.digital_id_wallet_api_wallet_catalog_response import ( + DigitalIdWalletApiWalletCatalogResponse as DigitalIdWalletApiWalletCatalogResponse, + ) + from didit.types.components.digital_id_wallet_api_wallet_identity import ( + DigitalIdWalletApiWalletIdentity as DigitalIdWalletApiWalletIdentity, + ) + from didit.types.components.digital_id_wallet_api_wallet_next_action import ( + DigitalIdWalletApiWalletNextAction as DigitalIdWalletApiWalletNextAction, + ) + from didit.types.components.digital_id_wallet_api_wallet_next_action_type_enum import ( + DigitalIdWalletApiWalletNextActionTypeEnum as DigitalIdWalletApiWalletNextActionTypeEnum, + ) + from didit.types.components.digital_id_wallet_api_wallet_verification_response import ( + DigitalIdWalletApiWalletVerificationResponse as DigitalIdWalletApiWalletVerificationResponse, + ) + from didit.types.components.digital_id_wallet_api_wallet_verification_result import ( + DigitalIdWalletApiWalletVerificationResult as DigitalIdWalletApiWalletVerificationResult, + ) + from didit.types.components.digital_id_wallet_api_wallet_verification_result_outcome_enum import ( + DigitalIdWalletApiWalletVerificationResultOutcomeEnum as DigitalIdWalletApiWalletVerificationResultOutcomeEnum, + ) + from didit.types.components.document_ai_detected_code import ( + DocumentAIDetectedCode as DocumentAIDetectedCode, + ) + from didit.types.components.email_intelligence import EmailIntelligence as EmailIntelligence + from didit.types.components.email_match_item import EmailMatchItem as EmailMatchItem + from didit.types.components.email_risk_response import EmailRiskResponse as EmailRiskResponse + from didit.types.components.email_risk_response import ( + EmailRiskResponseEmail as EmailRiskResponseEmail, + ) + from didit.types.components.email_risk_response import ( + EmailRiskResponseEmailBreaches as EmailRiskResponseEmailBreaches, + ) + from didit.types.components.email_social_footprint import ( + EmailSocialFootprint as EmailSocialFootprint, + ) + from didit.types.components.email_verification_check_response import ( + EmailVerificationCheckResponse as EmailVerificationCheckResponse, + ) + from didit.types.components.email_verification_check_response import ( + EmailVerificationCheckResponseEmail as EmailVerificationCheckResponseEmail, + ) + from didit.types.components.email_verification_check_response import ( + EmailVerificationCheckResponseEmailBreaches as EmailVerificationCheckResponseEmailBreaches, + ) + from didit.types.components.email_verification_check_response import ( + EmailVerificationCheckResponseEmailLifecycle as EmailVerificationCheckResponseEmailLifecycle, + ) + from didit.types.components.email_verification_check_response import ( + EmailVerificationCheckResponseEmailMatches as EmailVerificationCheckResponseEmailMatches, + ) + from didit.types.components.email_verification_check_response import ( + EmailVerificationCheckResponseEmailWarnings as EmailVerificationCheckResponseEmailWarnings, + ) + from didit.types.components.email_verification_send_response import ( + EmailVerificationSendResponse as EmailVerificationSendResponse, + ) + from didit.types.components.entity_network_membership import ( + EntityNetworkMembership as EntityNetworkMembership, + ) + from didit.types.components.fraud_availability_enum import ( + FraudAvailabilityEnum as FraudAvailabilityEnum, + ) + from didit.types.components.fraud_band_enum import FraudBandEnum as FraudBandEnum + from didit.types.components.fraud_check_attributes_request import ( + FraudCheckAttributesRequest as FraudCheckAttributesRequest, + ) + from didit.types.components.fraud_check_context_request import ( + FraudCheckContextRequest as FraudCheckContextRequest, + ) + from didit.types.components.fraud_check_identifier_request import ( + FraudCheckIdentifierRequest as FraudCheckIdentifierRequest, + ) + from didit.types.components.fraud_check_identity_relationship import ( + FraudCheckIdentityRelationship as FraudCheckIdentityRelationship, + ) + from didit.types.components.fraud_check_input import FraudCheckInput as FraudCheckInput + from didit.types.components.fraud_check_module_report import ( + FraudCheckModuleReport as FraudCheckModuleReport, + ) + from didit.types.components.fraud_check_module_selection_request import ( + FraudCheckModuleSelectionRequest as FraudCheckModuleSelectionRequest, + ) + from didit.types.components.fraud_check_plan import FraudCheckPlan as FraudCheckPlan + from didit.types.components.fraud_check_planned_module import ( + FraudCheckPlannedModule as FraudCheckPlannedModule, + ) + from didit.types.components.fraud_check_response import FraudCheckResponse as FraudCheckResponse + from didit.types.components.fraud_check_risk import FraudCheckRisk as FraudCheckRisk + from didit.types.components.fraud_check_subject_request import ( + FraudCheckSubjectRequest as FraudCheckSubjectRequest, + ) + from didit.types.components.fraud_dimension_enum import FraudDimensionEnum as FraudDimensionEnum + from didit.types.components.fraud_exclude_enum import FraudExcludeEnum as FraudExcludeEnum + from didit.types.components.fraud_next_action_enum import ( + FraudNextActionEnum as FraudNextActionEnum, + ) + from didit.types.components.fraud_processing_status_enum import ( + FraudProcessingStatusEnum as FraudProcessingStatusEnum, + ) + from didit.types.components.fraud_recommendation_enum import ( + FraudRecommendationEnum as FraudRecommendationEnum, + ) + from didit.types.components.fraud_result_enum import FraudResultEnum as FraudResultEnum + from didit.types.components.fraud_status_enum import FraudStatusEnum as FraudStatusEnum + from didit.types.components.fraud_type_enum import FraudTypeEnum as FraudTypeEnum + from didit.types.components.ip_risk_response import IPRiskResponse as IPRiskResponse + from didit.types.components.ip_risk_response import IPRiskResponseIp as IPRiskResponseIp + from didit.types.components.kyb_registry_address import KYBRegistryAddress as KYBRegistryAddress + from didit.types.components.kyb_registry_industry import ( + KYBRegistryIndustry as KYBRegistryIndustry, + ) + from didit.types.components.kyb_registry_ownership import ( + KYBRegistryOwnership as KYBRegistryOwnership, + ) + from didit.types.components.kyb_registry_ownership import ( + KYBRegistryOwnershipUnresolved as KYBRegistryOwnershipUnresolved, + ) + from didit.types.components.kyb_registry_shareholding import ( + KYBRegistryShareholding as KYBRegistryShareholding, + ) + from didit.types.components.login_response import LoginResponse as LoginResponse + from didit.types.components.network_detail import NetworkDetail as NetworkDetail + from didit.types.components.network_graph import NetworkGraph as NetworkGraph + from didit.types.components.network_graph import NetworkGraphMetrics as NetworkGraphMetrics + from didit.types.components.network_graph_edge import NetworkGraphEdge as NetworkGraphEdge + from didit.types.components.network_graph_node import NetworkGraphNode as NetworkGraphNode + from didit.types.components.network_list_item import NetworkListItem as NetworkListItem + from didit.types.components.network_map import NetworkMap as NetworkMap + from didit.types.components.network_map import NetworkMapPoints as NetworkMapPoints + from didit.types.components.network_member import NetworkMember as NetworkMember + from didit.types.components.network_member import ( + NetworkMemberProvenance as NetworkMemberProvenance, + ) + from didit.types.components.network_node_tag import NetworkNodeTag as NetworkNodeTag + from didit.types.components.network_note import NetworkNote as NetworkNote + from didit.types.components.network_risk_display import NetworkRiskDisplay as NetworkRiskDisplay + from didit.types.components.network_signal import NetworkSignal as NetworkSignal + from didit.types.components.network_timeline_event import ( + NetworkTimelineEvent as NetworkTimelineEvent, + ) + from didit.types.components.organization import Organization as Organization + from didit.types.components.organization_summary import ( + OrganizationSummary as OrganizationSummary, + ) + from didit.types.components.phone_intelligence import PhoneIntelligence as PhoneIntelligence + from didit.types.components.phone_match_item import PhoneMatchItem as PhoneMatchItem + from didit.types.components.phone_risk_response import PhoneRiskResponse as PhoneRiskResponse + from didit.types.components.phone_risk_response import ( + PhoneRiskResponsePhone as PhoneRiskResponsePhone, + ) + from didit.types.components.phone_risk_response import ( + PhoneRiskResponsePhoneCarrier as PhoneRiskResponsePhoneCarrier, + ) + from didit.types.components.phone_social_footprint import ( + PhoneSocialFootprint as PhoneSocialFootprint, + ) + from didit.types.components.phone_verification_check_response import ( + PhoneVerificationCheckResponse as PhoneVerificationCheckResponse, + ) + from didit.types.components.phone_verification_check_response import ( + PhoneVerificationCheckResponsePhone as PhoneVerificationCheckResponsePhone, + ) + from didit.types.components.phone_verification_check_response import ( + PhoneVerificationCheckResponsePhoneCarrier as PhoneVerificationCheckResponsePhoneCarrier, + ) + from didit.types.components.phone_verification_check_response import ( + PhoneVerificationCheckResponsePhoneLifecycle as PhoneVerificationCheckResponsePhoneLifecycle, + ) + from didit.types.components.phone_verification_check_response import ( + PhoneVerificationCheckResponsePhoneMatches as PhoneVerificationCheckResponsePhoneMatches, + ) + from didit.types.components.phone_verification_check_response import ( + PhoneVerificationCheckResponsePhoneWarnings as PhoneVerificationCheckResponsePhoneWarnings, + ) + from didit.types.components.phone_verification_send_response import ( + PhoneVerificationSendResponse as PhoneVerificationSendResponse, + ) + from didit.types.components.questionnaire_detail import ( + QuestionnaireDetail as QuestionnaireDetail, + ) + from didit.types.components.questionnaire_list_item import ( + QuestionnaireListItem as QuestionnaireListItem, + ) + from didit.types.components.register_response import RegisterResponse as RegisterResponse + from didit.types.components.regulatory_report_detail import ( + RegulatoryReportDetail as RegulatoryReportDetail, + ) + from didit.types.components.regulatory_report_detail import ( + RegulatoryReportDetailNarrative as RegulatoryReportDetailNarrative, + ) + from didit.types.components.regulatory_report_detail import ( + RegulatoryReportDetailValidationErrors as RegulatoryReportDetailValidationErrors, + ) + from didit.types.components.report_template_detail import ( + ReportTemplateDetail as ReportTemplateDetail, + ) + from didit.types.components.review_item import ReviewItem as ReviewItem + from didit.types.components.risk_lifecycle_event import RiskLifecycleEvent as RiskLifecycleEvent + from didit.types.components.risk_signals import RiskSignals as RiskSignals + from didit.types.components.risk_warning import RiskWarning as RiskWarning + from didit.types.components.routing_publication_case_routing_rule import ( + RoutingPublicationCaseRoutingRule as RoutingPublicationCaseRoutingRule, + ) + from didit.types.components.routing_publication_field_enum import ( + RoutingPublicationFieldEnum as RoutingPublicationFieldEnum, + ) + from didit.types.components.routing_publication_operator_enum import ( + RoutingPublicationOperatorEnum as RoutingPublicationOperatorEnum, + ) + from didit.types.components.routing_publication_paginated_policy_revision_list import ( + RoutingPublicationPaginatedPolicyRevisionList as RoutingPublicationPaginatedPolicyRevisionList, + ) + from didit.types.components.routing_publication_policy_draft import ( + RoutingPublicationPolicyDraft as RoutingPublicationPolicyDraft, + ) + from didit.types.components.routing_publication_policy_draft_response import ( + RoutingPublicationPolicyDraftResponse as RoutingPublicationPolicyDraftResponse, + ) + from didit.types.components.routing_publication_policy_revision import ( + RoutingPublicationPolicyRevision as RoutingPublicationPolicyRevision, + ) + from didit.types.components.routing_publication_routing_condition import ( + RoutingPublicationRoutingCondition as RoutingPublicationRoutingCondition, + ) + from didit.types.components.session_import_error import SessionImportError as SessionImportError + from didit.types.components.session_import_job import SessionImportJob as SessionImportJob + from didit.types.components.simple_questionnaire_element import ( + SimpleQuestionnaireElement as SimpleQuestionnaireElement, + ) + from didit.types.components.simple_questionnaire_element import ( + SimpleQuestionnaireElementOptions as SimpleQuestionnaireElementOptions, + ) + from didit.types.components.simple_questionnaire_element import ( + SimpleQuestionnaireElementValidation as SimpleQuestionnaireElementValidation, + ) + from didit.types.components.simple_workflow_feature import ( + SimpleWorkflowFeature as SimpleWorkflowFeature, + ) + from didit.types.components.social_footprint_category_summary import ( + SocialFootprintCategorySummary as SocialFootprintCategorySummary, + ) + from didit.types.components.social_footprint_platform import ( + SocialFootprintPlatform as SocialFootprintPlatform, + ) + from didit.types.components.social_footprint_profile import ( + SocialFootprintProfile as SocialFootprintProfile, + ) + from didit.types.components.transaction_detail import TransactionDetail as TransactionDetail + from didit.types.components.transaction_detail import ( + TransactionDetailActionRequiredVerificationSession as TransactionDetailActionRequiredVerificationSession, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailActionRequiredWalletOwnership as TransactionDetailActionRequiredWalletOwnership, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailActivities as TransactionDetailActivities, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailAlerts as TransactionDetailAlerts, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailDocuments as TransactionDetailDocuments, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailDocumentsFiles as TransactionDetailDocumentsFiles, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailNetworkSnapshot as TransactionDetailNetworkSnapshot, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailParties as TransactionDetailParties, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailPartiesDevice as TransactionDetailPartiesDevice, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailPartiesDeviceNetworkContext as TransactionDetailPartiesDeviceNetworkContext, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailPaymentMethods as TransactionDetailPaymentMethods, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailProviderResults as TransactionDetailProviderResults, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailRemediation as TransactionDetailRemediation, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailRuleRuns as TransactionDetailRuleRuns, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailTags as TransactionDetailTags, + ) + from didit.types.components.transaction_detail import ( + TransactionDetailTravelRule as TransactionDetailTravelRule, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleAction as TransactionRuleAction, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionParam as TransactionRuleActionParam, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant1 as TransactionRuleActionVariant1, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant1Param as TransactionRuleActionVariant1Param, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant2 as TransactionRuleActionVariant2, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant2Param as TransactionRuleActionVariant2Param, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant3 as TransactionRuleActionVariant3, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant3Param as TransactionRuleActionVariant3Param, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant4 as TransactionRuleActionVariant4, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant4Param as TransactionRuleActionVariant4Param, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant5 as TransactionRuleActionVariant5, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant5Param as TransactionRuleActionVariant5Param, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant6 as TransactionRuleActionVariant6, + ) + from didit.types.components.transaction_rule_action import ( + TransactionRuleActionVariant6Param as TransactionRuleActionVariant6Param, + ) + from didit.types.components.transaction_rule_aggregation import ( + TransactionRuleAggregation as TransactionRuleAggregation, + ) + from didit.types.components.transaction_rule_aggregation import ( + TransactionRuleAggregationParam as TransactionRuleAggregationParam, + ) + from didit.types.components.transaction_rule_backtest_result import ( + TransactionRuleBacktestResult as TransactionRuleBacktestResult, + ) + from didit.types.components.transaction_rule_condition import ( + TransactionRuleCondition as TransactionRuleCondition, + ) + from didit.types.components.transaction_rule_condition import ( + TransactionRuleConditionParam as TransactionRuleConditionParam, + ) + from didit.types.components.transaction_rule_detail import ( + TransactionRuleDetail as TransactionRuleDetail, + ) + from didit.types.components.transaction_rule_install_result import ( + TransactionRuleInstallResult as TransactionRuleInstallResult, + ) + from didit.types.components.transaction_rule_library_entry import ( + TransactionRuleLibraryEntry as TransactionRuleLibraryEntry, + ) + from didit.types.components.transaction_rule_scope import ( + TransactionRuleScope as TransactionRuleScope, + ) + from didit.types.components.transaction_rule_scope import ( + TransactionRuleScopeParam as TransactionRuleScopeParam, + ) + from didit.types.components.travel_rule_inbound_response import ( + TravelRuleInboundResponse as TravelRuleInboundResponse, + ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetail as TravelRuleSettingsDetail, + ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetailNetworks as TravelRuleSettingsDetailNetworks, + ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetailNetworksGtr as TravelRuleSettingsDetailNetworksGtr, + ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetailNetworksSygna as TravelRuleSettingsDetailNetworksSygna, + ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetailNetworksTrust as TravelRuleSettingsDetailNetworksTrust, + ) + from didit.types.components.travel_rule_settings_detail import ( + TravelRuleSettingsDetailNetworksVerifyvasp as TravelRuleSettingsDetailNetworksVerifyvasp, + ) + from didit.types.components.travel_rule_transfer_detail import ( + TravelRuleTransferDetail as TravelRuleTransferDetail, + ) + from didit.types.components.travel_rule_transfer_detail import ( + TravelRuleTransferDetailCounterpartyVasp as TravelRuleTransferDetailCounterpartyVasp, + ) + from didit.types.components.travel_rule_transfer_detail import ( + TravelRuleTransferDetailTimeline as TravelRuleTransferDetailTimeline, + ) + from didit.types.components.travel_rule_widget_session_response import ( + TravelRuleWidgetSessionResponse as TravelRuleWidgetSessionResponse, + ) + from didit.types.components.user_detail_item import UserDetailItem as UserDetailItem + from didit.types.components.user_detail_item import ( + UserDetailItemComments as UserDetailItemComments, + ) + from didit.types.components.user_list_item import UserListItem as UserListItem + from didit.types.components.user_list_item import ( + UserListItemFeaturesList as UserListItemFeaturesList, + ) + from didit.types.components.user_list_item import UserListItemTags as UserListItemTags + from didit.types.components.vasp_directory_entry import VaspDirectoryEntry as VaspDirectoryEntry + from didit.types.components.verify_email_response import ( + VerifyEmailResponse as VerifyEmailResponse, + ) + from didit.types.components.wallet_address_entry import WalletAddressEntry as WalletAddressEntry + from didit.types.components.wallet_address_entry_update import ( + WalletAddressEntryUpdate as WalletAddressEntryUpdate, + ) + from didit.types.components.wallet_screening_result import ( + WalletScreeningResult as WalletScreeningResult, + ) + from didit.types.components.wallet_screening_result import ( + WalletScreeningResultCounterpartyConnections as WalletScreeningResultCounterpartyConnections, + ) + from didit.types.components.wallet_screening_result import ( + WalletScreeningResultDestinationOfFunds as WalletScreeningResultDestinationOfFunds, + ) + from didit.types.components.wallet_screening_result import ( + WalletScreeningResultRiskFactors as WalletScreeningResultRiskFactors, + ) + from didit.types.components.wallet_screening_result import ( + WalletScreeningResultSourceOfFunds as WalletScreeningResultSourceOfFunds, + ) + from didit.types.components.webhook_event import WebhookEvent as WebhookEvent + from didit.types.components.webhook_subscribed_events import ( + WebhookSubscribedEvents as WebhookSubscribedEvents, + ) + from didit.types.components.webhook_subscribed_events import ( + WebhookSubscribedEventsParam as WebhookSubscribedEventsParam, + ) + from didit.types.components.workflow_list_item import WorkflowListItem as WorkflowListItem + from didit.types.components.workflow_list_item import ( + WorkflowListItemCreatedBy as WorkflowListItemCreatedBy, + ) + from didit.types.customization.update_params import ( + CustomizationUpdateParams as CustomizationUpdateParams, + ) + from didit.types.database_validation.validate_params import ( + DatabaseValidationValidateParams as DatabaseValidationValidateParams, + ) + from didit.types.database_validation.validate_params import ( + DatabaseValidationValidateParamsAddressVariant1 as DatabaseValidationValidateParamsAddressVariant1, + ) + from didit.types.database_validation.validate_response import ( + DatabaseValidationValidateResponse as DatabaseValidationValidateResponse, + ) + from didit.types.database_validation.validate_response import ( + DatabaseValidationValidateResponseDatabaseValidation as DatabaseValidationValidateResponseDatabaseValidation, + ) + from didit.types.database_validation.validate_response import ( + DatabaseValidationValidateResponseDatabaseValidationErrors as DatabaseValidationValidateResponseDatabaseValidationErrors, + ) + from didit.types.database_validation.validate_response import ( + DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1 as DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1, + ) + from didit.types.database_validation.validate_response import ( + DatabaseValidationValidateResponseDatabaseValidationWarnings as DatabaseValidationValidateResponseDatabaseValidationWarnings, + ) + from didit.types.document_ai.analyze_params import ( + DocumentAiAnalyzeParams as DocumentAiAnalyzeParams, + ) + from didit.types.document_ai.analyze_response import ( + DocumentAiAnalyzeResponse as DocumentAiAnalyzeResponse, + ) + from didit.types.document_ai.analyze_response import ( + DocumentAiAnalyzeResponseDocumentAi as DocumentAiAnalyzeResponseDocumentAi, + ) + from didit.types.document_ai.analyze_response import ( + DocumentAiAnalyzeResponseDocumentAiFields as DocumentAiAnalyzeResponseDocumentAiFields, + ) + from didit.types.document_ai.analyze_response import ( + DocumentAiAnalyzeResponseDocumentAiWarnings as DocumentAiAnalyzeResponseDocumentAiWarnings, + ) + from didit.types.email.assess_risk_params import EmailAssessRiskParams as EmailAssessRiskParams + from didit.types.email.check_code_params import EmailCheckCodeParams as EmailCheckCodeParams + from didit.types.email.send_code_params import EmailSendCodeParams as EmailSendCodeParams + from didit.types.email.send_code_params import ( + EmailSendCodeParamsOptions as EmailSendCodeParamsOptions, + ) + from didit.types.email.send_code_params import ( + EmailSendCodeParamsSignals as EmailSendCodeParamsSignals, + ) + from didit.types.face_match.compare_params import ( + FaceMatchCompareParams as FaceMatchCompareParams, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponse as FaceMatchCompareResponse, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponseFaceMatch as FaceMatchCompareResponseFaceMatch, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponseFaceMatchRefImage as FaceMatchCompareResponseFaceMatchRefImage, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponseFaceMatchRefImageEntities as FaceMatchCompareResponseFaceMatchRefImageEntities, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponseFaceMatchUserImage as FaceMatchCompareResponseFaceMatchUserImage, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponseFaceMatchUserImageEntities as FaceMatchCompareResponseFaceMatchUserImageEntities, + ) + from didit.types.face_match.compare_response import ( + FaceMatchCompareResponseFaceMatchWarnings as FaceMatchCompareResponseFaceMatchWarnings, + ) + from didit.types.face_search.search_params import ( + FaceSearchSearchParams as FaceSearchSearchParams, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponse as FaceSearchSearchResponse, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponseFaceSearch as FaceSearchSearchResponseFaceSearch, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponseFaceSearchMatches as FaceSearchSearchResponseFaceSearchMatches, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponseFaceSearchMatchesUserDetails as FaceSearchSearchResponseFaceSearchMatchesUserDetails, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponseFaceSearchUserImage as FaceSearchSearchResponseFaceSearchUserImage, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponseFaceSearchUserImageEntities as FaceSearchSearchResponseFaceSearchUserImageEntities, + ) + from didit.types.face_search.search_response import ( + FaceSearchSearchResponseFaceSearchWarnings as FaceSearchSearchResponseFaceSearchWarnings, + ) + from didit.types.fraud_checks.create_params import ( + FraudChecksCreateParams as FraudChecksCreateParams, + ) + from didit.types.fraud_checks.plan_params import FraudChecksPlanParams as FraudChecksPlanParams + from didit.types.id_verification.verify_params import ( + IdVerificationVerifyParams as IdVerificationVerifyParams, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponse as IdVerificationVerifyResponse, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerification as IdVerificationVerifyResponseIdVerification, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationBackImageQualityScore as IdVerificationVerifyResponseIdVerificationBackImageQualityScore, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationBarcodes as IdVerificationVerifyResponseIdVerificationBarcodes, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationFrontImageQualityScore as IdVerificationVerifyResponseIdVerificationFrontImageQualityScore, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationMrz as IdVerificationVerifyResponseIdVerificationMrz, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationParsedAddress as IdVerificationVerifyResponseIdVerificationParsedAddress, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation as IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation, + ) + from didit.types.id_verification.verify_response import ( + IdVerificationVerifyResponseIdVerificationWarnings as IdVerificationVerifyResponseIdVerificationWarnings, + ) + from didit.types.id_wallets.start_audkenni_params import ( + IdWalletsStartAudkenniParams as IdWalletsStartAudkenniParams, + ) + from didit.types.id_wallets.start_bankid_cz_params import ( + IdWalletsStartBankidCzParams as IdWalletsStartBankidCzParams, + ) + from didit.types.id_wallets.start_bankid_no_params import ( + IdWalletsStartBankidNoParams as IdWalletsStartBankidNoParams, + ) + from didit.types.id_wallets.start_bankid_se_params import ( + IdWalletsStartBankidSeParams as IdWalletsStartBankidSeParams, + ) + from didit.types.id_wallets.start_buypass_params import ( + IdWalletsStartBuypassParams as IdWalletsStartBuypassParams, + ) + from didit.types.id_wallets.start_connectid_params import ( + IdWalletsStartConnectidParams as IdWalletsStartConnectidParams, + ) + from didit.types.id_wallets.start_diia_params import ( + IdWalletsStartDiiaParams as IdWalletsStartDiiaParams, + ) + from didit.types.id_wallets.start_eparaksts_mobile_params import ( + IdWalletsStartEparakstsMobileParams as IdWalletsStartEparakstsMobileParams, + ) + from didit.types.id_wallets.start_eudi_params import ( + IdWalletsStartEudiParams as IdWalletsStartEudiParams, + ) + from didit.types.id_wallets.start_franceconnect_params import ( + IdWalletsStartFranceconnectParams as IdWalletsStartFranceconnectParams, + ) + from didit.types.id_wallets.start_frejaid_params import ( + IdWalletsStartFrejaidParams as IdWalletsStartFrejaidParams, + ) + from didit.types.id_wallets.start_ftn_params import ( + IdWalletsStartFtnParams as IdWalletsStartFtnParams, + ) + from didit.types.id_wallets.start_govbr_params import ( + IdWalletsStartGovbrParams as IdWalletsStartGovbrParams, + ) + from didit.types.id_wallets.start_govuk_wallet_params import ( + IdWalletsStartGovukWalletParams as IdWalletsStartGovukWalletParams, + ) + from didit.types.id_wallets.start_idcard_ee_params import ( + IdWalletsStartIdcardEeParams as IdWalletsStartIdcardEeParams, + ) + from didit.types.id_wallets.start_idin_params import ( + IdWalletsStartIdinParams as IdWalletsStartIdinParams, + ) + from didit.types.id_wallets.start_itsme_params import ( + IdWalletsStartItsmeParams as IdWalletsStartItsmeParams, + ) + from didit.types.id_wallets.start_mitid_params import ( + IdWalletsStartMitidParams as IdWalletsStartMitidParams, + ) + from didit.types.id_wallets.start_mobile_id_params import ( + IdWalletsStartMobileIdParams as IdWalletsStartMobileIdParams, + ) + from didit.types.id_wallets.start_mojeid_params import ( + IdWalletsStartMojeidParams as IdWalletsStartMojeidParams, + ) + from didit.types.id_wallets.start_oneid_params import ( + IdWalletsStartOneidParams as IdWalletsStartOneidParams, + ) + from didit.types.id_wallets.start_personalausweis_params import ( + IdWalletsStartPersonalausweisParams as IdWalletsStartPersonalausweisParams, + ) + from didit.types.id_wallets.start_smart_id_params import ( + IdWalletsStartSmartIdParams as IdWalletsStartSmartIdParams, + ) + from didit.types.id_wallets.start_uae_pass_params import ( + IdWalletsStartUaePassParams as IdWalletsStartUaePassParams, + ) + from didit.types.id_wallets.start_vipps_params import ( + IdWalletsStartVippsParams as IdWalletsStartVippsParams, + ) + from didit.types.ip.assess_risk_params import IpAssessRiskParams as IpAssessRiskParams + from didit.types.kyb.list_shareholders_params import ( + KybListShareholdersParams as KybListShareholdersParams, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse as KybListShareholdersResponse, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse202 as KybListShareholdersResponse202, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse202KybRegistry as KybListShareholdersResponse202KybRegistry, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse202KybRegistryBeneficialOwners as KybListShareholdersResponse202KybRegistryBeneficialOwners, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse202KybRegistryOfficers as KybListShareholdersResponse202KybRegistryOfficers, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponseKybRegistry as KybListShareholdersResponseKybRegistry, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponseKybRegistryBeneficialOwners as KybListShareholdersResponseKybRegistryBeneficialOwners, + ) + from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponseKybRegistryOfficers as KybListShareholdersResponseKybRegistryOfficers, + ) + from didit.types.kyb.list_ubos_params import KybListUbosParams as KybListUbosParams + from didit.types.kyb.list_ubos_response import KybListUbosResponse as KybListUbosResponse + from didit.types.kyb.list_ubos_response import KybListUbosResponse202 as KybListUbosResponse202 + from didit.types.kyb.list_ubos_response import ( + KybListUbosResponse202KybRegistry as KybListUbosResponse202KybRegistry, + ) + from didit.types.kyb.list_ubos_response import ( + KybListUbosResponse202KybRegistryBeneficialOwners as KybListUbosResponse202KybRegistryBeneficialOwners, + ) + from didit.types.kyb.list_ubos_response import ( + KybListUbosResponse202KybRegistryOfficers as KybListUbosResponse202KybRegistryOfficers, + ) + from didit.types.kyb.list_ubos_response import ( + KybListUbosResponseKybRegistry as KybListUbosResponseKybRegistry, + ) + from didit.types.kyb.list_ubos_response import ( + KybListUbosResponseKybRegistryBeneficialOwners as KybListUbosResponseKybRegistryBeneficialOwners, + ) + from didit.types.kyb.list_ubos_response import ( + KybListUbosResponseKybRegistryOfficers as KybListUbosResponseKybRegistryOfficers, + ) + from didit.types.kyb.search_params import KybSearchParams as KybSearchParams + from didit.types.kyb.search_response import KybSearchResponse as KybSearchResponse + from didit.types.kyb.search_response import ( + KybSearchResponseKybRegistry as KybSearchResponseKybRegistry, + ) + from didit.types.kyb.search_response import ( + KybSearchResponseKybRegistryCompanies as KybSearchResponseKybRegistryCompanies, + ) + from didit.types.kyb.search_response import ( + KybSearchResponseKybRegistryPagination as KybSearchResponseKybRegistryPagination, + ) + from didit.types.kyb.select_params import KybSelectParams as KybSelectParams + from didit.types.kyb.select_response import KybSelectResponse as KybSelectResponse + from didit.types.kyb.select_response import ( + KybSelectResponseKybRegistry as KybSelectResponseKybRegistry, + ) + from didit.types.kyb.select_response import ( + KybSelectResponseKybRegistryBeneficialOwners as KybSelectResponseKybRegistryBeneficialOwners, + ) + from didit.types.kyb.select_response import ( + KybSelectResponseKybRegistryOfficers as KybSelectResponseKybRegistryOfficers, + ) + from didit.types.networks.list_members_response import ( + NetworksListMembersResponse as NetworksListMembersResponse, + ) + from didit.types.networks.list_response import NetworksListResponse as NetworksListResponse + from didit.types.networks.list_response import ( + NetworksListResponseKpis as NetworksListResponseKpis, + ) + from didit.types.networks.list_signals_response import ( + NetworksListSignalsResponse as NetworksListSignalsResponse, + ) + from didit.types.networks.notes.create_params import ( + NetworksNotesCreateParams as NetworksNotesCreateParams, + ) + from didit.types.networks.notes.list_response import ( + NetworksNotesListResponse as NetworksNotesListResponse, + ) + from didit.types.networks.retrieve_timeline_response import ( + NetworksRetrieveTimelineResponse as NetworksRetrieveTimelineResponse, + ) + from didit.types.passive_liveness.check_params import ( + PassiveLivenessCheckParams as PassiveLivenessCheckParams, + ) + from didit.types.passive_liveness.check_response import ( + PassiveLivenessCheckResponse as PassiveLivenessCheckResponse, + ) + from didit.types.passive_liveness.check_response import ( + PassiveLivenessCheckResponseLiveness as PassiveLivenessCheckResponseLiveness, + ) + from didit.types.passive_liveness.check_response import ( + PassiveLivenessCheckResponseLivenessUserImage as PassiveLivenessCheckResponseLivenessUserImage, + ) + from didit.types.passive_liveness.check_response import ( + PassiveLivenessCheckResponseLivenessUserImageEntities as PassiveLivenessCheckResponseLivenessUserImageEntities, + ) + from didit.types.passive_liveness.check_response import ( + PassiveLivenessCheckResponseLivenessWarnings as PassiveLivenessCheckResponseLivenessWarnings, + ) + from didit.types.phone.assess_risk_params import PhoneAssessRiskParams as PhoneAssessRiskParams + from didit.types.phone.check_code_params import PhoneCheckCodeParams as PhoneCheckCodeParams + from didit.types.phone.send_code_params import PhoneSendCodeParams as PhoneSendCodeParams + from didit.types.phone.send_code_params import ( + PhoneSendCodeParamsOptions as PhoneSendCodeParamsOptions, + ) + from didit.types.phone.send_code_params import ( + PhoneSendCodeParamsSignals as PhoneSendCodeParamsSignals, + ) + from didit.types.proof_of_address.verify_params import ( + ProofOfAddressVerifyParams as ProofOfAddressVerifyParams, + ) + from didit.types.proof_of_address.verify_response import ( + ProofOfAddressVerifyResponse as ProofOfAddressVerifyResponse, + ) + from didit.types.proof_of_address.verify_response import ( + ProofOfAddressVerifyResponsePoa as ProofOfAddressVerifyResponsePoa, + ) + from didit.types.proof_of_address.verify_response import ( + ProofOfAddressVerifyResponsePoaExtraFields as ProofOfAddressVerifyResponsePoaExtraFields, + ) + from didit.types.proof_of_address.verify_response import ( + ProofOfAddressVerifyResponsePoaPoaParsedAddress as ProofOfAddressVerifyResponsePoaPoaParsedAddress, + ) + from didit.types.proof_of_address.verify_response import ( + ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation as ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation, + ) + from didit.types.proof_of_address.verify_response import ( + ProofOfAddressVerifyResponsePoaWarnings as ProofOfAddressVerifyResponsePoaWarnings, + ) + from didit.types.questionnaires.create_params import ( + QuestionnairesCreateParams as QuestionnairesCreateParams, + ) + from didit.types.questionnaires.list_response import ( + QuestionnairesListResponse as QuestionnairesListResponse, + ) + from didit.types.questionnaires.update_params import ( + QuestionnairesUpdateParams as QuestionnairesUpdateParams, + ) + from didit.types.regulatory_reports.create_params import ( + RegulatoryReportsCreateParams as RegulatoryReportsCreateParams, + ) + from didit.types.regulatory_reports.create_params import ( + RegulatoryReportsCreateParamsNarrative as RegulatoryReportsCreateParamsNarrative, + ) + from didit.types.regulatory_reports.list_response import ( + RegulatoryReportsListResponse as RegulatoryReportsListResponse, + ) + from didit.types.regulatory_reports.retrieve_download_url_response import ( + RegulatoryReportsRetrieveDownloadUrlResponse as RegulatoryReportsRetrieveDownloadUrlResponse, + ) + from didit.types.regulatory_reports.update_params import ( + RegulatoryReportsUpdateParams as RegulatoryReportsUpdateParams, + ) + from didit.types.regulatory_reports.update_params import ( + RegulatoryReportsUpdateParamsNarrative as RegulatoryReportsUpdateParamsNarrative, + ) + from didit.types.regulatory_reports.validate_response import ( + RegulatoryReportsValidateResponse as RegulatoryReportsValidateResponse, + ) + from didit.types.regulatory_reports.validate_response import ( + RegulatoryReportsValidateResponseErrors as RegulatoryReportsValidateResponseErrors, + ) + from didit.types.report_templates.create_params import ( + ReportTemplatesCreateParams as ReportTemplatesCreateParams, + ) + from didit.types.report_templates.list_response import ( + ReportTemplatesListResponse as ReportTemplatesListResponse, + ) + from didit.types.report_templates.update_params import ( + ReportTemplatesUpdateParams as ReportTemplatesUpdateParams, + ) + from didit.types.sessions.batch_delete_params import ( + SessionsBatchDeleteParams as SessionsBatchDeleteParams, + ) + from didit.types.sessions.batch_delete_response import ( + SessionsBatchDeleteResponse as SessionsBatchDeleteResponse, + ) + from didit.types.sessions.batch_delete_response import ( + SessionsBatchDeleteResponse202 as SessionsBatchDeleteResponse202, + ) + from didit.types.sessions.batch_delete_response import ( + SessionsBatchDeleteResponse202Outcomes as SessionsBatchDeleteResponse202Outcomes, + ) + from didit.types.sessions.batch_delete_response import ( + SessionsBatchDeleteResponseResults as SessionsBatchDeleteResponseResults, + ) + from didit.types.sessions.bulk_update_aml_hit_status_params import ( + SessionsBulkUpdateAmlHitStatusParams as SessionsBulkUpdateAmlHitStatusParams, + ) + from didit.types.sessions.bulk_update_aml_hit_status_params import ( + SessionsBulkUpdateAmlHitStatusParamsHitUpdates as SessionsBulkUpdateAmlHitStatusParamsHitUpdates, + ) + from didit.types.sessions.create_params import SessionsCreateParams as SessionsCreateParams + from didit.types.sessions.create_params import ( + SessionsCreateParamsContactDetails as SessionsCreateParamsContactDetails, + ) + from didit.types.sessions.create_params import ( + SessionsCreateParamsExpectedDetails as SessionsCreateParamsExpectedDetails, + ) + from didit.types.sessions.create_response import ( + SessionsCreateResponse as SessionsCreateResponse, + ) + from didit.types.sessions.delete_params import SessionsDeleteParams as SessionsDeleteParams + from didit.types.sessions.delete_response import ( + SessionsDeleteResponse as SessionsDeleteResponse, + ) + from didit.types.sessions.import_shared_params import ( + SessionsImportSharedParams as SessionsImportSharedParams, + ) + from didit.types.sessions.imports.create_params import ( + SessionsImportsCreateParams as SessionsImportsCreateParams, + ) + from didit.types.sessions.imports.list_errors_response import ( + SessionsImportsListErrorsResponse as SessionsImportsListErrorsResponse, + ) + from didit.types.sessions.list_networks_response import ( + SessionsListNetworksResponse as SessionsListNetworksResponse, + ) + from didit.types.sessions.list_response import SessionsListResponse as SessionsListResponse + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKybSessionRowSessionKindBusiness as SessionsListResponseResultsKybSessionRowSessionKindBusiness, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUser as SessionsListResponseResultsKycSessionRowSessionKindUser, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa as SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress as SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserFeatures as SessionsListResponseResultsKycSessionRowSessionKindUserFeatures, + ) + from didit.types.sessions.list_response import ( + SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber as SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber, + ) + from didit.types.sessions.preview_resubmission_params import ( + SessionsPreviewResubmissionParams as SessionsPreviewResubmissionParams, + ) + from didit.types.sessions.preview_resubmission_params import ( + SessionsPreviewResubmissionParamsNodesToResubmit as SessionsPreviewResubmissionParamsNodesToResubmit, + ) + from didit.types.sessions.preview_resubmission_response import ( + SessionsPreviewResubmissionResponse as SessionsPreviewResubmissionResponse, + ) + from didit.types.sessions.preview_resubmission_response import ( + SessionsPreviewResubmissionResponseNodesToResubmit as SessionsPreviewResubmissionResponseNodesToResubmit, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponse as SessionsRetrieveDecisionResponse, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreenings as SessionsRetrieveDecisionResponseAmlScreenings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHits as SessionsRetrieveDecisionResponseAmlScreeningsHits, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails as SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities as SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties as SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes as SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown as SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails as SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsScreenedData as SessionsRetrieveDecisionResponseAmlScreeningsScreenedData, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseAmlScreeningsWarnings as SessionsRetrieveDecisionResponseAmlScreeningsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseContactDetails as SessionsRetrieveDecisionResponseContactDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDatabaseValidations as SessionsRetrieveDecisionResponseDatabaseValidations, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDatabaseValidationsErrors as SessionsRetrieveDecisionResponseDatabaseValidationsErrors, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData as SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDatabaseValidationsValidations as SessionsRetrieveDecisionResponseDatabaseValidationsValidations, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation as SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDatabaseValidationsWarnings as SessionsRetrieveDecisionResponseDatabaseValidationsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentAiDocuments as SessionsRetrieveDecisionResponseDocumentAiDocuments, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentAiDocumentsItems as SessionsRetrieveDecisionResponseDocumentAiDocumentsItems, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields as SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentVerifications as SessionsRetrieveDecisionResponseDocumentVerifications, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue as SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentVerificationsItems as SessionsRetrieveDecisionResponseDocumentVerificationsItems, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata as SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation as SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions as SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseEmailVerifications as SessionsRetrieveDecisionResponseEmailVerifications, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseEmailVerificationsBreaches as SessionsRetrieveDecisionResponseEmailVerificationsBreaches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseEmailVerificationsLifecycle as SessionsRetrieveDecisionResponseEmailVerificationsLifecycle, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails as SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseEmailVerificationsMatches as SessionsRetrieveDecisionResponseEmailVerificationsMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseEmailVerificationsWarnings as SessionsRetrieveDecisionResponseEmailVerificationsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseExpectedDetails as SessionsRetrieveDecisionResponseExpectedDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseFaceMatches as SessionsRetrieveDecisionResponseFaceMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseFaceMatchesWarnings as SessionsRetrieveDecisionResponseFaceMatchesWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerifications as SessionsRetrieveDecisionResponseIdVerifications, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore as SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness as SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace as SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy as SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay as SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches as SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails as SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsExtraFields as SessionsRetrieveDecisionResponseIdVerificationsExtraFields, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom as SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore as SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsIdLookup as SessionsRetrieveDecisionResponseIdVerificationsIdLookup, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison as SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsMatches as SessionsRetrieveDecisionResponseIdVerificationsMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails as SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsMrz as SessionsRetrieveDecisionResponseIdVerificationsMrz, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddress as SessionsRetrieveDecisionResponseIdVerificationsParsedAddress, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation as SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsWalletVerification as SessionsRetrieveDecisionResponseIdVerificationsWalletVerification, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIdVerificationsWarnings as SessionsRetrieveDecisionResponseIdVerificationsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalyses as SessionsRetrieveDecisionResponseIpAnalyses, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesIdDocument as SessionsRetrieveDecisionResponseIpAnalysesIdDocument, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation as SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesIp as SessionsRetrieveDecisionResponseIpAnalysesIp, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesIpLocation as SessionsRetrieveDecisionResponseIpAnalysesIpLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesMatches as SessionsRetrieveDecisionResponseIpAnalysesMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo as SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo as SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesPoaDocument as SessionsRetrieveDecisionResponseIpAnalysesPoaDocument, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation as SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseIpAnalysesWarnings as SessionsRetrieveDecisionResponseIpAnalysesWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecks as SessionsRetrieveDecisionResponseKeyPeopleChecks, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures as SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures as SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary as SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseLivenessChecks as SessionsRetrieveDecisionResponseLivenessChecks, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseLivenessChecksMatches as SessionsRetrieveDecisionResponseLivenessChecksMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails as SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseLivenessChecksWarnings as SessionsRetrieveDecisionResponseLivenessChecksWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseNfcVerifications as SessionsRetrieveDecisionResponseNfcVerifications, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity as SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary as SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseNfcVerificationsChipData as SessionsRetrieveDecisionResponseNfcVerificationsChipData, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseNfcVerificationsWarnings as SessionsRetrieveDecisionResponseNfcVerificationsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePhoneVerifications as SessionsRetrieveDecisionResponsePhoneVerifications, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePhoneVerificationsCarrier as SessionsRetrieveDecisionResponsePhoneVerificationsCarrier, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle as SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails as SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePhoneVerificationsMatches as SessionsRetrieveDecisionResponsePhoneVerificationsMatches, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePhoneVerificationsWarnings as SessionsRetrieveDecisionResponsePhoneVerificationsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerifications as SessionsRetrieveDecisionResponsePoaVerifications, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata as SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation as SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions as SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsExtraFields as SessionsRetrieveDecisionResponsePoaVerificationsExtraFields, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress as SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation as SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponsePoaVerificationsWarnings as SessionsRetrieveDecisionResponsePoaVerificationsWarnings, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseQuestionnaireResponses as SessionsRetrieveDecisionResponseQuestionnaireResponses, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseQuestionnaireResponsesSections as SessionsRetrieveDecisionResponseQuestionnaireResponsesSections, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems as SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer as SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices as SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseRegistryChecks as SessionsRetrieveDecisionResponseRegistryChecks, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseRegistryChecksCompany as SessionsRetrieveDecisionResponseRegistryChecksCompany, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners as SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers as SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseResubmissions as SessionsRetrieveDecisionResponseResubmissions, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseResubmissionsNodes as SessionsRetrieveDecisionResponseResubmissionsNodes, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseResubmissionsRequestedBy as SessionsRetrieveDecisionResponseResubmissionsRequestedBy, + ) + from didit.types.sessions.retrieve_decision_response import ( + SessionsRetrieveDecisionResponseReviews as SessionsRetrieveDecisionResponseReviews, + ) + from didit.types.sessions.retrieve_deletion_job_response import ( + SessionsRetrieveDeletionJobResponse as SessionsRetrieveDeletionJobResponse, + ) + from didit.types.sessions.retrieve_deletion_job_response import ( + SessionsRetrieveDeletionJobResponseOutcomes as SessionsRetrieveDeletionJobResponseOutcomes, + ) + from didit.types.sessions.reviews.create_params import ( + SessionsReviewsCreateParams as SessionsReviewsCreateParams, + ) + from didit.types.sessions.reviews.create_response import ( + SessionsReviewsCreateResponse as SessionsReviewsCreateResponse, + ) + from didit.types.sessions.reviews.list_response import ( + SessionsReviewsListResponse as SessionsReviewsListResponse, + ) + from didit.types.sessions.share_params import SessionsShareParams as SessionsShareParams + from didit.types.sessions.share_response import SessionsShareResponse as SessionsShareResponse + from didit.types.sessions.update_aml_hit_status_params import ( + SessionsUpdateAmlHitStatusParams as SessionsUpdateAmlHitStatusParams, + ) + from didit.types.sessions.update_data_params import ( + SessionsUpdateDataParams as SessionsUpdateDataParams, + ) + from didit.types.sessions.update_data_params import ( + SessionsUpdateDataParamsParsedAddress as SessionsUpdateDataParamsParsedAddress, + ) + from didit.types.sessions.update_data_response import ( + SessionsUpdateDataResponse as SessionsUpdateDataResponse, + ) + from didit.types.sessions.update_data_response import ( + SessionsUpdateDataResponseParsedAddress as SessionsUpdateDataResponseParsedAddress, + ) + from didit.types.sessions.update_feature_status_params import ( + SessionsUpdateFeatureStatusParams as SessionsUpdateFeatureStatusParams, + ) + from didit.types.sessions.update_feature_status_response import ( + SessionsUpdateFeatureStatusResponse as SessionsUpdateFeatureStatusResponse, + ) + from didit.types.sessions.update_kyb_company_data_params import ( + SessionsUpdateKybCompanyDataParams as SessionsUpdateKybCompanyDataParams, + ) + from didit.types.sessions.update_kyb_company_data_response import ( + SessionsUpdateKybCompanyDataResponse as SessionsUpdateKybCompanyDataResponse, + ) + from didit.types.sessions.update_kyb_party_params import ( + SessionsUpdateKybPartyParams as SessionsUpdateKybPartyParams, + ) + from didit.types.sessions.update_kyb_party_response import ( + SessionsUpdateKybPartyResponse as SessionsUpdateKybPartyResponse, + ) + from didit.types.sessions.update_poa_data_params import ( + SessionsUpdatePoaDataParams as SessionsUpdatePoaDataParams, + ) + from didit.types.sessions.update_poa_data_params import ( + SessionsUpdatePoaDataParamsPoaParsedAddress as SessionsUpdatePoaDataParamsPoaParsedAddress, + ) + from didit.types.sessions.update_poa_data_response import ( + SessionsUpdatePoaDataResponse as SessionsUpdatePoaDataResponse, + ) + from didit.types.sessions.update_poa_data_response import ( + SessionsUpdatePoaDataResponsePoaParsedAddress as SessionsUpdatePoaDataResponsePoaParsedAddress, + ) + from didit.types.sessions.update_status_params import ( + SessionsUpdateStatusParams as SessionsUpdateStatusParams, + ) + from didit.types.sessions.update_status_params import ( + SessionsUpdateStatusParamsNodesToResubmit as SessionsUpdateStatusParamsNodesToResubmit, + ) + from didit.types.sessions.update_status_response import ( + SessionsUpdateStatusResponse as SessionsUpdateStatusResponse, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParams as TransactionsCreateParams, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsCounterparty as TransactionsCreateParamsCounterparty, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsCounterpartyDeviceContext as TransactionsCreateParamsCounterpartyDeviceContext, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsCounterpartyDeviceContextNetworkContext as TransactionsCreateParamsCounterpartyDeviceContextNetworkContext, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsCounterpartyPaymentMethod as TransactionsCreateParamsCounterpartyPaymentMethod, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsNetworkSnapshot as TransactionsCreateParamsNetworkSnapshot, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsSubject as TransactionsCreateParamsSubject, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsSubjectAddress as TransactionsCreateParamsSubjectAddress, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsSubjectDeviceContext as TransactionsCreateParamsSubjectDeviceContext, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsSubjectDeviceContextNetworkContext as TransactionsCreateParamsSubjectDeviceContextNetworkContext, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsSubjectInstitutionDetails as TransactionsCreateParamsSubjectInstitutionDetails, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsSubjectPaymentMethod as TransactionsCreateParamsSubjectPaymentMethod, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsTransactionDetails as TransactionsCreateParamsTransactionDetails, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsTransactionDetailsCryptoParams as TransactionsCreateParamsTransactionDetailsCryptoParams, + ) + from didit.types.transactions.create_params import ( + TransactionsCreateParamsTravelRuleDetails as TransactionsCreateParamsTravelRuleDetails, + ) + from didit.types.transactions.create_sdk_token_params import ( + TransactionsCreateSdkTokenParams as TransactionsCreateSdkTokenParams, + ) + from didit.types.transactions.create_sdk_token_response import ( + TransactionsCreateSdkTokenResponse as TransactionsCreateSdkTokenResponse, + ) + from didit.types.transactions.list_networks_response import ( + TransactionsListNetworksResponse as TransactionsListNetworksResponse, + ) + from didit.types.transactions.list_party_networks_response import ( + TransactionsListPartyNetworksResponse as TransactionsListPartyNetworksResponse, + ) + from didit.types.transactions.list_response import ( + TransactionsListResponse as TransactionsListResponse, + ) + from didit.types.transactions.list_response import ( + TransactionsListResponseResults as TransactionsListResponseResults, + ) + from didit.types.transactions.list_response import ( + TransactionsListResponseResultsTags as TransactionsListResponseResultsTags, + ) + from didit.types.transactions.rules.backtest_params import ( + TransactionsRulesBacktestParams as TransactionsRulesBacktestParams, + ) + from didit.types.transactions.rules.create_params import ( + TransactionsRulesCreateParams as TransactionsRulesCreateParams, + ) + from didit.types.transactions.rules.install_library_params import ( + TransactionsRulesInstallLibraryParams as TransactionsRulesInstallLibraryParams, + ) + from didit.types.transactions.rules.list_library_response import ( + TransactionsRulesListLibraryResponse as TransactionsRulesListLibraryResponse, + ) + from didit.types.transactions.rules.list_response import ( + TransactionsRulesListResponse as TransactionsRulesListResponse, + ) + from didit.types.transactions.rules.uninstall_library_params import ( + TransactionsRulesUninstallLibraryParams as TransactionsRulesUninstallLibraryParams, + ) + from didit.types.transactions.rules.update_params import ( + TransactionsRulesUpdateParams as TransactionsRulesUpdateParams, + ) + from didit.types.transactions.travel_rule.confirm_ownership_params import ( + TransactionsTravelRuleConfirmOwnershipParams as TransactionsTravelRuleConfirmOwnershipParams, + ) + from didit.types.transactions.travel_rule.update_transfer_params import ( + TransactionsTravelRuleUpdateTransferParams as TransactionsTravelRuleUpdateTransferParams, + ) + from didit.types.travel_rule.create_widget_session_params import ( + TravelRuleCreateWidgetSessionParams as TravelRuleCreateWidgetSessionParams, + ) + from didit.types.travel_rule.list_vasps_response import ( + TravelRuleListVaspsResponse as TravelRuleListVaspsResponse, + ) + from didit.types.travel_rule.register_inbound_params import ( + TravelRuleRegisterInboundParams as TravelRuleRegisterInboundParams, + ) + from didit.types.travel_rule.register_inbound_params import ( + TravelRuleRegisterInboundParamsOriginatingVasp as TravelRuleRegisterInboundParamsOriginatingVasp, + ) + from didit.types.travel_rule.update_settings_params import ( + TravelRuleUpdateSettingsParams as TravelRuleUpdateSettingsParams, + ) + from didit.types.travel_rule.wallet_addresses.create_params import ( + TravelRuleWalletAddressesCreateParams as TravelRuleWalletAddressesCreateParams, + ) + from didit.types.travel_rule.wallet_addresses.list_response import ( + TravelRuleWalletAddressesListResponse as TravelRuleWalletAddressesListResponse, + ) + from didit.types.travel_rule.wallet_addresses.update_params import ( + TravelRuleWalletAddressesUpdateParams as TravelRuleWalletAddressesUpdateParams, + ) + from didit.types.users.batch_delete_params import ( + UsersBatchDeleteParams as UsersBatchDeleteParams, + ) + from didit.types.users.batch_delete_response import ( + UsersBatchDeleteResponse as UsersBatchDeleteResponse, + ) + from didit.types.users.create_params import UsersCreateParams as UsersCreateParams + from didit.types.users.list_networks_response import ( + UsersListNetworksResponse as UsersListNetworksResponse, + ) + from didit.types.users.list_response import UsersListResponse as UsersListResponse + from didit.types.users.retrieve_ongoing_monitoring_job_response import ( + UsersRetrieveOngoingMonitoringJobResponse as UsersRetrieveOngoingMonitoringJobResponse, + ) + from didit.types.users.retrieve_ongoing_monitoring_job_response import ( + UsersRetrieveOngoingMonitoringJobResponseEstimate as UsersRetrieveOngoingMonitoringJobResponseEstimate, + ) + from didit.types.users.retrieve_ongoing_monitoring_job_response import ( + UsersRetrieveOngoingMonitoringJobResponseOutcomes as UsersRetrieveOngoingMonitoringJobResponseOutcomes, + ) + from didit.types.users.set_aml_monitoring_params import ( + UsersSetAmlMonitoringParams as UsersSetAmlMonitoringParams, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponse202 as UsersSetAmlMonitoringResponse202, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponse202Estimate as UsersSetAmlMonitoringResponse202Estimate, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponse202Outcomes as UsersSetAmlMonitoringResponse202Outcomes, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponse202Results as UsersSetAmlMonitoringResponse202Results, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponseVariant1 as UsersSetAmlMonitoringResponseVariant1, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponseVariant2 as UsersSetAmlMonitoringResponseVariant2, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponseVariant2Estimate as UsersSetAmlMonitoringResponseVariant2Estimate, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponseVariant2Outcomes as UsersSetAmlMonitoringResponseVariant2Outcomes, + ) + from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponseVariant2Results as UsersSetAmlMonitoringResponseVariant2Results, + ) + from didit.types.users.set_ongoing_monitoring_params import ( + UsersSetOngoingMonitoringParams as UsersSetOngoingMonitoringParams, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponse202 as UsersSetOngoingMonitoringResponse202, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponse202Estimate as UsersSetOngoingMonitoringResponse202Estimate, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponse202Outcomes as UsersSetOngoingMonitoringResponse202Outcomes, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponseVariant1 as UsersSetOngoingMonitoringResponseVariant1, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponseVariant2 as UsersSetOngoingMonitoringResponseVariant2, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponseVariant2Estimate as UsersSetOngoingMonitoringResponseVariant2Estimate, + ) + from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponseVariant2Outcomes as UsersSetOngoingMonitoringResponseVariant2Outcomes, + ) + from didit.types.users.update_params import UsersUpdateParams as UsersUpdateParams + from didit.types.users.update_status_params import ( + UsersUpdateStatusParams as UsersUpdateStatusParams, + ) + from didit.types.users.upload_face_params import UsersUploadFaceParams as UsersUploadFaceParams + from didit.types.users.upload_face_response import ( + UsersUploadFaceResponse as UsersUploadFaceResponse, + ) + from didit.types.wallet_screening.screen_params import ( + WalletScreeningScreenParams as WalletScreeningScreenParams, + ) + from didit.types.wallet_screening.screen_pdf_params import ( + WalletScreeningScreenPdfParams as WalletScreeningScreenPdfParams, + ) + from didit.types.wallet_screening.screen_pdf_params import ( + WalletScreeningScreenPdfParamsCounterpartyConnections as WalletScreeningScreenPdfParamsCounterpartyConnections, + ) + from didit.types.wallet_screening.screen_pdf_params import ( + WalletScreeningScreenPdfParamsDestinationOfFunds as WalletScreeningScreenPdfParamsDestinationOfFunds, + ) + from didit.types.wallet_screening.screen_pdf_params import ( + WalletScreeningScreenPdfParamsRiskFactors as WalletScreeningScreenPdfParamsRiskFactors, + ) + from didit.types.wallet_screening.screen_pdf_params import ( + WalletScreeningScreenPdfParamsSourceOfFunds as WalletScreeningScreenPdfParamsSourceOfFunds, + ) + from didit.types.webhooks.destinations.create_params import ( + WebhooksDestinationsCreateParams as WebhooksDestinationsCreateParams, + ) + from didit.types.webhooks.destinations.create_response import ( + WebhooksDestinationsCreateResponse as WebhooksDestinationsCreateResponse, + ) + from didit.types.webhooks.destinations.create_response import ( + WebhooksDestinationsCreateResponseSummary as WebhooksDestinationsCreateResponseSummary, + ) + from didit.types.webhooks.destinations.list_response import ( + WebhooksDestinationsListResponseItem as WebhooksDestinationsListResponseItem, + ) + from didit.types.webhooks.destinations.retrieve_response import ( + WebhooksDestinationsRetrieveResponse as WebhooksDestinationsRetrieveResponse, + ) + from didit.types.webhooks.destinations.retrieve_response import ( + WebhooksDestinationsRetrieveResponseSummary as WebhooksDestinationsRetrieveResponseSummary, + ) + from didit.types.webhooks.destinations.update_params import ( + WebhooksDestinationsUpdateParams as WebhooksDestinationsUpdateParams, + ) + from didit.types.webhooks.destinations.update_response import ( + WebhooksDestinationsUpdateResponse as WebhooksDestinationsUpdateResponse, + ) + from didit.types.webhooks.destinations.update_response import ( + WebhooksDestinationsUpdateResponseSummary as WebhooksDestinationsUpdateResponseSummary, + ) + from didit.types.workflows.create_params import WorkflowsCreateParams as WorkflowsCreateParams + from didit.types.workflows.list_response import WorkflowsListResponse as WorkflowsListResponse + from didit.types.workflows.update_params import WorkflowsUpdateParams as WorkflowsUpdateParams + +_MODULES: dict[str, str] = { + "AccountApplicationsCreateParams": "account.applications.create_params", + "AccountApplicationsUpdateParams": "account.applications.update_params", + "AccountLoginParams": "account.login_params", + "AccountRegisterParams": "account.register_params", + "AccountResendOtpParams": "account.resend_otp_params", + "AccountVerifyEmailParams": "account.verify_email_params", + "AgeEstimationEstimateParams": "age_estimation.estimate_params", + "AgeEstimationEstimateResponse": "age_estimation.estimate_response", + "AgeEstimationEstimateResponseAgeEstimation": "age_estimation.estimate_response", + "AgeEstimationEstimateResponseAgeEstimationUserImage": "age_estimation.estimate_response", + "AgeEstimationEstimateResponseAgeEstimationUserImageEntities": "age_estimation.estimate_response", + "AgeEstimationEstimateResponseAgeEstimationWarnings": "age_estimation.estimate_response", + "AmlMonitoringRetrieveJobResponse": "aml_monitoring.retrieve_job_response", + "AmlMonitoringRetrieveJobResponseOutcomes": "aml_monitoring.retrieve_job_response", + "AmlMonitoringRetrieveJobResponseResults": "aml_monitoring.retrieve_job_response", + "AmlScreenParams": "aml.screen_params", + "AmlScreenResponse": "aml.screen_response", + "AmlScreenResponseAml": "aml.screen_response", + "AmlScreenResponseAmlHits": "aml.screen_response", + "AmlScreenResponseAmlHitsAdverseMediaMatches": "aml.screen_response", + "AmlScreenResponseAmlHitsLinkedEntities": "aml.screen_response", + "AmlScreenResponseAmlHitsPepMatches": "aml.screen_response", + "AmlScreenResponseAmlHitsScoreBreakdown": "aml.screen_response", + "AmlScreenResponseAmlScreenedData": "aml.screen_response", + "AmlScreenResponseAmlWarnings": "aml.screen_response", + "Application": "components.application", + "ApplicationCredentialsSummary": "components.application_credentials_summary", + "BillingRetrieveBalanceResponse": "billing.retrieve_balance_response", + "BillingTopUpParams": "billing.top_up_params", + "BillingTopUpResponse": "billing.top_up_response", + "BiometricTemplatesBatchDeleteParams": "biometric_templates.batch_delete_params", + "BiometricTemplatesBatchDeleteResponse": "biometric_templates.batch_delete_response", + "BiometricTemplatesBatchDeleteResponseResults": "biometric_templates.batch_delete_response", + "BiometricTemplatesCountResponse": "biometric_templates.count_response", + "BiometricTemplatesDeleteResponse": "biometric_templates.delete_response", + "BiometricTemplatesListResponse": "biometric_templates.list_response", + "BiometricTemplatesListResponseResults": "biometric_templates.list_response", + "BiometricTemplatesRetrieveResponse": "biometric_templates.retrieve_response", + "BrandingCustomization": "components.branding_customization", + "BusinessDetailItem": "components.business_detail_item", + "BusinessListItem": "components.business_list_item", + "BusinessListItemFeaturesList": "components.business_list_item", + "BusinessListItemTags": "components.business_list_item", + "BusinessSessionsListNetworksResponse": "business_sessions.list_networks_response", + "BusinessesBatchDeleteParams": "businesses.batch_delete_params", + "BusinessesBatchDeleteResponse": "businesses.batch_delete_response", + "BusinessesCreateParams": "businesses.create_params", + "BusinessesListNetworksResponse": "businesses.list_networks_response", + "BusinessesListResponse": "businesses.list_response", + "BusinessesRetrieveOngoingMonitoringJobResponse": "businesses.retrieve_ongoing_monitoring_job_response", + "BusinessesRetrieveOngoingMonitoringJobResponseEstimate": "businesses.retrieve_ongoing_monitoring_job_response", + "BusinessesRetrieveOngoingMonitoringJobResponseOutcomes": "businesses.retrieve_ongoing_monitoring_job_response", + "BusinessesRetrieveSessionOngoingMonitoringJobResponse": "businesses.retrieve_session_ongoing_monitoring_job_response", + "BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate": "businesses.retrieve_session_ongoing_monitoring_job_response", + "BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes": "businesses.retrieve_session_ongoing_monitoring_job_response", + "BusinessesSetAmlMonitoringParams": "businesses.set_aml_monitoring_params", + "BusinessesSetAmlMonitoringResponse202": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponse202Estimate": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponse202Outcomes": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponse202Results": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponseVariant1": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponseVariant2": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponseVariant2Estimate": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponseVariant2Outcomes": "businesses.set_aml_monitoring_response", + "BusinessesSetAmlMonitoringResponseVariant2Results": "businesses.set_aml_monitoring_response", + "BusinessesSetOngoingMonitoringParams": "businesses.set_ongoing_monitoring_params", + "BusinessesSetOngoingMonitoringResponse202": "businesses.set_ongoing_monitoring_response", + "BusinessesSetOngoingMonitoringResponse202Estimate": "businesses.set_ongoing_monitoring_response", + "BusinessesSetOngoingMonitoringResponse202Outcomes": "businesses.set_ongoing_monitoring_response", + "BusinessesSetOngoingMonitoringResponseVariant1": "businesses.set_ongoing_monitoring_response", + "BusinessesSetOngoingMonitoringResponseVariant2": "businesses.set_ongoing_monitoring_response", + "BusinessesSetOngoingMonitoringResponseVariant2Estimate": "businesses.set_ongoing_monitoring_response", + "BusinessesSetOngoingMonitoringResponseVariant2Outcomes": "businesses.set_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringParams": "businesses.set_session_ongoing_monitoring_params", + "BusinessesSetSessionOngoingMonitoringResponse202": "businesses.set_session_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringResponse202Estimate": "businesses.set_session_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringResponse202Outcomes": "businesses.set_session_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringResponseVariant1": "businesses.set_session_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringResponseVariant2": "businesses.set_session_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate": "businesses.set_session_ongoing_monitoring_response", + "BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes": "businesses.set_session_ongoing_monitoring_response", + "BusinessesUpdateParams": "businesses.update_params", + "BusinessesUpdateStatusParams": "businesses.update_status_params", + "CaseActionActionEnum": "components.case_action_action_enum", + "CaseActionCaseActionReceipt": "components.case_action_case_action_receipt", + "CaseActionCaseActionReceiptPage": "components.case_action_case_action_receipt_page", + "CaseActionCaseEntityActionResult": "components.case_action_case_entity_action_result", + "CaseActionEntityTypeEnum": "components.case_action_entity_type_enum", + "CaseActionStatusEnum": "components.case_action_status_enum", + "CaseAssignee": "components.case_assignee", + "CaseBlueprintDetail": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfig": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigAmlControl": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigChecklist": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigChecklistItems": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigFinancial": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigIdentity": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigReporting": "components.case_blueprint_detail", + "CaseBlueprintDetailContentConfigVerifications": "components.case_blueprint_detail", + "CaseBlueprintDetailCreationSources": "components.case_blueprint_detail", + "CaseBlueprintDetailReceivesEscalationFrom": "components.case_blueprint_detail", + "CaseBlueprintListItem": "components.case_blueprint_list_item", + "CaseBlueprintSummary": "components.case_blueprint_summary", + "CaseChecklistItem": "components.case_checklist_item", + "CaseCollaborationCaseNote": "components.case_collaboration_case_note", + "CaseCollaborationCaseNoteReply": "components.case_collaboration_case_note_reply", + "CaseCollaborationPaginatedCaseNoteList": "components.case_collaboration_paginated_case_note_list", + "CaseContentConfig": "components.case_content_config", + "CaseContentConfigAmlControl": "components.case_content_config", + "CaseContentConfigAmlControlParam": "components.case_content_config", + "CaseContentConfigChecklist": "components.case_content_config", + "CaseContentConfigChecklistItems": "components.case_content_config", + "CaseContentConfigChecklistItemsParam": "components.case_content_config", + "CaseContentConfigChecklistParam": "components.case_content_config", + "CaseContentConfigFinancial": "components.case_content_config", + "CaseContentConfigFinancialParam": "components.case_content_config", + "CaseContentConfigIdentity": "components.case_content_config", + "CaseContentConfigIdentityParam": "components.case_content_config", + "CaseContentConfigParam": "components.case_content_config", + "CaseContentConfigReporting": "components.case_content_config", + "CaseContentConfigReportingParam": "components.case_content_config", + "CaseContentConfigVerifications": "components.case_content_config", + "CaseContentConfigVerificationsParam": "components.case_content_config", + "CaseDetail": "components.case_detail", + "CaseDetailPendingResolution": "components.case_detail", + "CaseDuplicateCaseDuplicate": "components.case_duplicate_case_duplicate", + "CaseDuplicateCaseDuplicatePage": "components.case_duplicate_case_duplicate_page", + "CaseDuplicateCaseFraudSourceContextRequest": "components.case_duplicate_case_fraud_source_context_request", + "CaseDuplicateCaseLinkCreateRequest": "components.case_duplicate_case_link_create_request", + "CaseDuplicateCaseSubjectInputRequest": "components.case_duplicate_case_subject_input_request", + "CaseDuplicateEntityTypeEnum": "components.case_duplicate_entity_type_enum", + "CaseDuplicatePriorityEnum": "components.case_duplicate_priority_enum", + "CaseDuplicateStatusEnum": "components.case_duplicate_status_enum", + "CaseDuplicateTypeEnum": "components.case_duplicate_type_enum", + "CaseEvent": "components.case_event", + "CaseLink": "components.case_link", + "CaseListItem": "components.case_list_item", + "CaseNote": "components.case_note", + "CaseNoteAttachments": "components.case_note", + "CasePublicationAssignmentMethodEnum": "components.case_publication_assignment_method_enum", + "CasePublicationCaseBlueprint": "components.case_publication_case_blueprint", + "CasePublicationPaginatedPolicyRevisionList": "components.case_publication_paginated_policy_revision_list", + "CasePublicationPolicyDraft": "components.case_publication_policy_draft", + "CasePublicationPolicyDraftResponse": "components.case_publication_policy_draft_response", + "CasePublicationPolicyRevision": "components.case_publication_policy_revision", + "CaseReviewTeam": "components.case_review_team", + "CaseReviewTeamMember": "components.case_review_team_member", + "CaseReviewTeamMemberParam": "components.case_review_team_member", + "CaseRoutingCondition": "components.case_routing_condition", + "CaseRoutingConditionParam": "components.case_routing_condition", + "CaseRoutingPreview": "components.case_routing_preview", + "CaseRoutingPreviewCandidates": "components.case_routing_preview", + "CaseRoutingPreviewMatchedRules": "components.case_routing_preview", + "CaseRoutingRule": "components.case_routing_rule", + "CaseRoutingRuleCreate": "components.case_routing_rule_create", + "CaseSubject": "components.case_subject", + "CasesApplyEntityActionParams": "cases.apply_entity_action_params", + "CasesAssignParams": "cases.assign_params", + "CasesBlueprintsCreateParams": "cases.blueprints.create_params", + "CasesBlueprintsDiscardDraftParams": "cases.blueprints.discard_draft_params", + "CasesBlueprintsInstallPresetsParams": "cases.blueprints.install_presets_params", + "CasesBlueprintsInstallPresetsResponse": "cases.blueprints.install_presets_response", + "CasesBlueprintsListPresetsResponse": "cases.blueprints.list_presets_response", + "CasesBlueprintsListPresetsResponseResults": "cases.blueprints.list_presets_response", + "CasesBlueprintsListResponse": "cases.blueprints.list_response", + "CasesBlueprintsPublishParams": "cases.blueprints.publish_params", + "CasesBlueprintsSaveDraftParams": "cases.blueprints.save_draft_params", + "CasesBlueprintsUpdateParams": "cases.blueprints.update_params", + "CasesBulkAssignParams": "cases.bulk_assign_params", + "CasesBulkAssignResponse": "cases.bulk_assign_response", + "CasesBulkAssignResponseFailures": "cases.bulk_assign_response", + "CasesBulkUpdateStatusParams": "cases.bulk_update_status_params", + "CasesBulkUpdateStatusResponse": "cases.bulk_update_status_response", + "CasesBulkUpdateStatusResponseFailures": "cases.bulk_update_status_response", + "CasesChecklistToggleItemParams": "cases.checklist.toggle_item_params", + "CasesCreateParams": "cases.create_params", + "CasesCreateParamsSubject": "cases.create_params", + "CasesFindDuplicatesParams": "cases.find_duplicates_params", + "CasesLinksCreateParams": "cases.links.create_params", + "CasesListEventsResponse": "cases.list_events_response", + "CasesListResponse": "cases.list_response", + "CasesListReviewersResponse": "cases.list_reviewers_response", + "CasesListReviewersResponseResults": "cases.list_reviewers_response", + "CasesNotesCreateAttachmentUploadParams": "cases.notes.create_attachment_upload_params", + "CasesNotesCreateAttachmentUploadResponse": "cases.notes.create_attachment_upload_response", + "CasesNotesCreateParams": "cases.notes.create_params", + "CasesNotesRetrieveAttachmentUrlResponse": "cases.notes.retrieve_attachment_url_response", + "CasesNotesUpdateParams": "cases.notes.update_params", + "CasesPrintResponse": "cases.print_response", + "CasesRegulatoryReportsCreateParams": "cases.regulatory_reports.create_params", + "CasesRegulatoryReportsCreateParamsNarrative": "cases.regulatory_reports.create_params", + "CasesRegulatoryReportsListResponse": "cases.regulatory_reports.list_response", + "CasesRejectResolutionParams": "cases.reject_resolution_params", + "CasesRequestsCreateParams": "cases.requests.create_params", + "CasesRequestsPreviewParams": "cases.requests.preview_params", + "CasesResolveParams": "cases.resolve_params", + "CasesRetrieveStatisticsResponse": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseQueue": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseSeries": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseTables": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseTablesBlueprint": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseTablesOfficer": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseTablesSource": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseTablesStatus": "cases.retrieve_statistics_response", + "CasesRetrieveStatisticsResponseTotals": "cases.retrieve_statistics_response", + "CasesRoutingRulesCreateParams": "cases.routing_rules.create_params", + "CasesRoutingRulesDiscardDraftParams": "cases.routing_rules.discard_draft_params", + "CasesRoutingRulesListResponse": "cases.routing_rules.list_response", + "CasesRoutingRulesPreviewParams": "cases.routing_rules.preview_params", + "CasesRoutingRulesPublishParams": "cases.routing_rules.publish_params", + "CasesRoutingRulesSaveDraftParams": "cases.routing_rules.save_draft_params", + "CasesRoutingRulesUpdateParams": "cases.routing_rules.update_params", + "CasesTeamsCreateParams": "cases.teams.create_params", + "CasesTeamsListResponse": "cases.teams.list_response", + "CasesTeamsUpdateParams": "cases.teams.update_params", + "CasesTransferParams": "cases.transfer_params", + "CasesUpdateParams": "cases.update_params", + "CustomerMessagingCaseRequest": "components.customer_messaging_case_request", + "CustomerMessagingCaseRequestContact": "components.customer_messaging_case_request_contact", + "CustomerMessagingCaseRequestOptions": "components.customer_messaging_case_request_options", + "CustomerMessagingCaseRequestPreviewResponse": "components.customer_messaging_case_request_preview_response", + "CustomerMessagingLanguageEnum": "components.customer_messaging_language_enum", + "CustomerMessagingLanguagesEnum": "components.customer_messaging_languages_enum", + "CustomerMessagingPaginatedCaseRequestList": "components.customer_messaging_paginated_case_request_list", + "CustomerMessagingPurposeEnum": "components.customer_messaging_purpose_enum", + "CustomerMessagingPurposesEnum": "components.customer_messaging_purposes_enum", + "CustomizationUpdateParams": "customization.update_params", + "DatabaseValidationValidateParams": "database_validation.validate_params", + "DatabaseValidationValidateParamsAddressVariant1": "database_validation.validate_params", + "DatabaseValidationValidateResponse": "database_validation.validate_response", + "DatabaseValidationValidateResponseDatabaseValidation": "database_validation.validate_response", + "DatabaseValidationValidateResponseDatabaseValidationErrors": "database_validation.validate_response", + "DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1": "database_validation.validate_response", + "DatabaseValidationValidateResponseDatabaseValidationWarnings": "database_validation.validate_response", + "DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_audkenni_wallet_verification_start_country_enum", + "DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_connectid_wallet_verification_start_country_enum", + "DigitalIdWalletApiCountry520Enum": "components.digital_id_wallet_api_country520_enum", + "DigitalIdWalletApiCountryAfcEnum": "components.digital_id_wallet_api_country_afc_enum", + "DigitalIdWalletApiCountryE2dEnum": "components.digital_id_wallet_api_country_e2d_enum", + "DigitalIdWalletApiCountryEe9Enum": "components.digital_id_wallet_api_country_ee9_enum", + "DigitalIdWalletApiDeviceEnum": "components.digital_id_wallet_api_device_enum", + "DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_diia_wallet_verification_start_country_enum", + "DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum", + "DigitalIdWalletApiEudiWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_eudi_wallet_verification_start_country_enum", + "DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum", + "DigitalIdWalletApiFtnWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_ftn_wallet_verification_start_country_enum", + "DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_govbr_wallet_verification_start_country_enum", + "DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum", + "DigitalIdWalletApiIdinWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_idin_wallet_verification_start_country_enum", + "DigitalIdWalletApiInteractionEnum": "components.digital_id_wallet_api_interaction_enum", + "DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_itsme_wallet_verification_start_country_enum", + "DigitalIdWalletApiMitidWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_mitid_wallet_verification_start_country_enum", + "DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum", + "DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum", + "DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_smart_id_wallet_verification_start_country_enum", + "DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum": "components.digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum", + "DigitalIdWalletApiWallet": "components.digital_id_wallet_api_wallet", + "DigitalIdWalletApiWalletCatalogEntry": "components.digital_id_wallet_api_wallet_catalog_entry", + "DigitalIdWalletApiWalletCatalogResponse": "components.digital_id_wallet_api_wallet_catalog_response", + "DigitalIdWalletApiWalletIdentity": "components.digital_id_wallet_api_wallet_identity", + "DigitalIdWalletApiWalletNextAction": "components.digital_id_wallet_api_wallet_next_action", + "DigitalIdWalletApiWalletNextActionTypeEnum": "components.digital_id_wallet_api_wallet_next_action_type_enum", + "DigitalIdWalletApiWalletVerificationResponse": "components.digital_id_wallet_api_wallet_verification_response", + "DigitalIdWalletApiWalletVerificationResult": "components.digital_id_wallet_api_wallet_verification_result", + "DigitalIdWalletApiWalletVerificationResultOutcomeEnum": "components.digital_id_wallet_api_wallet_verification_result_outcome_enum", + "DocumentAIDetectedCode": "components.document_ai_detected_code", + "DocumentAiAnalyzeParams": "document_ai.analyze_params", + "DocumentAiAnalyzeResponse": "document_ai.analyze_response", + "DocumentAiAnalyzeResponseDocumentAi": "document_ai.analyze_response", + "DocumentAiAnalyzeResponseDocumentAiFields": "document_ai.analyze_response", + "DocumentAiAnalyzeResponseDocumentAiWarnings": "document_ai.analyze_response", + "EmailAssessRiskParams": "email.assess_risk_params", + "EmailCheckCodeParams": "email.check_code_params", + "EmailIntelligence": "components.email_intelligence", + "EmailMatchItem": "components.email_match_item", + "EmailRiskResponse": "components.email_risk_response", + "EmailRiskResponseEmail": "components.email_risk_response", + "EmailRiskResponseEmailBreaches": "components.email_risk_response", + "EmailSendCodeParams": "email.send_code_params", + "EmailSendCodeParamsOptions": "email.send_code_params", + "EmailSendCodeParamsSignals": "email.send_code_params", + "EmailSocialFootprint": "components.email_social_footprint", + "EmailVerificationCheckResponse": "components.email_verification_check_response", + "EmailVerificationCheckResponseEmail": "components.email_verification_check_response", + "EmailVerificationCheckResponseEmailBreaches": "components.email_verification_check_response", + "EmailVerificationCheckResponseEmailLifecycle": "components.email_verification_check_response", + "EmailVerificationCheckResponseEmailMatches": "components.email_verification_check_response", + "EmailVerificationCheckResponseEmailWarnings": "components.email_verification_check_response", + "EmailVerificationSendResponse": "components.email_verification_send_response", + "EntityNetworkMembership": "components.entity_network_membership", + "FaceMatchCompareParams": "face_match.compare_params", + "FaceMatchCompareResponse": "face_match.compare_response", + "FaceMatchCompareResponseFaceMatch": "face_match.compare_response", + "FaceMatchCompareResponseFaceMatchRefImage": "face_match.compare_response", + "FaceMatchCompareResponseFaceMatchRefImageEntities": "face_match.compare_response", + "FaceMatchCompareResponseFaceMatchUserImage": "face_match.compare_response", + "FaceMatchCompareResponseFaceMatchUserImageEntities": "face_match.compare_response", + "FaceMatchCompareResponseFaceMatchWarnings": "face_match.compare_response", + "FaceSearchSearchParams": "face_search.search_params", + "FaceSearchSearchResponse": "face_search.search_response", + "FaceSearchSearchResponseFaceSearch": "face_search.search_response", + "FaceSearchSearchResponseFaceSearchMatches": "face_search.search_response", + "FaceSearchSearchResponseFaceSearchMatchesUserDetails": "face_search.search_response", + "FaceSearchSearchResponseFaceSearchUserImage": "face_search.search_response", + "FaceSearchSearchResponseFaceSearchUserImageEntities": "face_search.search_response", + "FaceSearchSearchResponseFaceSearchWarnings": "face_search.search_response", + "FraudAvailabilityEnum": "components.fraud_availability_enum", + "FraudBandEnum": "components.fraud_band_enum", + "FraudCheckAttributesRequest": "components.fraud_check_attributes_request", + "FraudCheckContextRequest": "components.fraud_check_context_request", + "FraudCheckIdentifierRequest": "components.fraud_check_identifier_request", + "FraudCheckIdentityRelationship": "components.fraud_check_identity_relationship", + "FraudCheckInput": "components.fraud_check_input", + "FraudCheckModuleReport": "components.fraud_check_module_report", + "FraudCheckModuleSelectionRequest": "components.fraud_check_module_selection_request", + "FraudCheckPlan": "components.fraud_check_plan", + "FraudCheckPlannedModule": "components.fraud_check_planned_module", + "FraudCheckResponse": "components.fraud_check_response", + "FraudCheckRisk": "components.fraud_check_risk", + "FraudCheckSubjectRequest": "components.fraud_check_subject_request", + "FraudChecksCreateParams": "fraud_checks.create_params", + "FraudChecksPlanParams": "fraud_checks.plan_params", + "FraudDimensionEnum": "components.fraud_dimension_enum", + "FraudExcludeEnum": "components.fraud_exclude_enum", + "FraudNextActionEnum": "components.fraud_next_action_enum", + "FraudProcessingStatusEnum": "components.fraud_processing_status_enum", + "FraudRecommendationEnum": "components.fraud_recommendation_enum", + "FraudResultEnum": "components.fraud_result_enum", + "FraudStatusEnum": "components.fraud_status_enum", + "FraudTypeEnum": "components.fraud_type_enum", + "IPRiskResponse": "components.ip_risk_response", + "IPRiskResponseIp": "components.ip_risk_response", + "IdVerificationVerifyParams": "id_verification.verify_params", + "IdVerificationVerifyResponse": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerification": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationBackImageQualityScore": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationBarcodes": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationFrontImageQualityScore": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationMrz": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationParsedAddress": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation": "id_verification.verify_response", + "IdVerificationVerifyResponseIdVerificationWarnings": "id_verification.verify_response", + "IdWalletsStartAudkenniParams": "id_wallets.start_audkenni_params", + "IdWalletsStartBankidCzParams": "id_wallets.start_bankid_cz_params", + "IdWalletsStartBankidNoParams": "id_wallets.start_bankid_no_params", + "IdWalletsStartBankidSeParams": "id_wallets.start_bankid_se_params", + "IdWalletsStartBuypassParams": "id_wallets.start_buypass_params", + "IdWalletsStartConnectidParams": "id_wallets.start_connectid_params", + "IdWalletsStartDiiaParams": "id_wallets.start_diia_params", + "IdWalletsStartEparakstsMobileParams": "id_wallets.start_eparaksts_mobile_params", + "IdWalletsStartEudiParams": "id_wallets.start_eudi_params", + "IdWalletsStartFranceconnectParams": "id_wallets.start_franceconnect_params", + "IdWalletsStartFrejaidParams": "id_wallets.start_frejaid_params", + "IdWalletsStartFtnParams": "id_wallets.start_ftn_params", + "IdWalletsStartGovbrParams": "id_wallets.start_govbr_params", + "IdWalletsStartGovukWalletParams": "id_wallets.start_govuk_wallet_params", + "IdWalletsStartIdcardEeParams": "id_wallets.start_idcard_ee_params", + "IdWalletsStartIdinParams": "id_wallets.start_idin_params", + "IdWalletsStartItsmeParams": "id_wallets.start_itsme_params", + "IdWalletsStartMitidParams": "id_wallets.start_mitid_params", + "IdWalletsStartMobileIdParams": "id_wallets.start_mobile_id_params", + "IdWalletsStartMojeidParams": "id_wallets.start_mojeid_params", + "IdWalletsStartOneidParams": "id_wallets.start_oneid_params", + "IdWalletsStartPersonalausweisParams": "id_wallets.start_personalausweis_params", + "IdWalletsStartSmartIdParams": "id_wallets.start_smart_id_params", + "IdWalletsStartUaePassParams": "id_wallets.start_uae_pass_params", + "IdWalletsStartVippsParams": "id_wallets.start_vipps_params", + "IpAssessRiskParams": "ip.assess_risk_params", + "KYBRegistryAddress": "components.kyb_registry_address", + "KYBRegistryIndustry": "components.kyb_registry_industry", + "KYBRegistryOwnership": "components.kyb_registry_ownership", + "KYBRegistryOwnershipUnresolved": "components.kyb_registry_ownership", + "KYBRegistryShareholding": "components.kyb_registry_shareholding", + "KybListShareholdersParams": "kyb.list_shareholders_params", + "KybListShareholdersResponse": "kyb.list_shareholders_response", + "KybListShareholdersResponse202": "kyb.list_shareholders_response", + "KybListShareholdersResponse202KybRegistry": "kyb.list_shareholders_response", + "KybListShareholdersResponse202KybRegistryBeneficialOwners": "kyb.list_shareholders_response", + "KybListShareholdersResponse202KybRegistryOfficers": "kyb.list_shareholders_response", + "KybListShareholdersResponseKybRegistry": "kyb.list_shareholders_response", + "KybListShareholdersResponseKybRegistryBeneficialOwners": "kyb.list_shareholders_response", + "KybListShareholdersResponseKybRegistryOfficers": "kyb.list_shareholders_response", + "KybListUbosParams": "kyb.list_ubos_params", + "KybListUbosResponse": "kyb.list_ubos_response", + "KybListUbosResponse202": "kyb.list_ubos_response", + "KybListUbosResponse202KybRegistry": "kyb.list_ubos_response", + "KybListUbosResponse202KybRegistryBeneficialOwners": "kyb.list_ubos_response", + "KybListUbosResponse202KybRegistryOfficers": "kyb.list_ubos_response", + "KybListUbosResponseKybRegistry": "kyb.list_ubos_response", + "KybListUbosResponseKybRegistryBeneficialOwners": "kyb.list_ubos_response", + "KybListUbosResponseKybRegistryOfficers": "kyb.list_ubos_response", + "KybSearchParams": "kyb.search_params", + "KybSearchResponse": "kyb.search_response", + "KybSearchResponseKybRegistry": "kyb.search_response", + "KybSearchResponseKybRegistryCompanies": "kyb.search_response", + "KybSearchResponseKybRegistryPagination": "kyb.search_response", + "KybSelectParams": "kyb.select_params", + "KybSelectResponse": "kyb.select_response", + "KybSelectResponseKybRegistry": "kyb.select_response", + "KybSelectResponseKybRegistryBeneficialOwners": "kyb.select_response", + "KybSelectResponseKybRegistryOfficers": "kyb.select_response", + "LoginResponse": "components.login_response", + "NetworkDetail": "components.network_detail", + "NetworkGraph": "components.network_graph", + "NetworkGraphEdge": "components.network_graph_edge", + "NetworkGraphMetrics": "components.network_graph", + "NetworkGraphNode": "components.network_graph_node", + "NetworkListItem": "components.network_list_item", + "NetworkMap": "components.network_map", + "NetworkMapPoints": "components.network_map", + "NetworkMember": "components.network_member", + "NetworkMemberProvenance": "components.network_member", + "NetworkNodeTag": "components.network_node_tag", + "NetworkNote": "components.network_note", + "NetworkRiskDisplay": "components.network_risk_display", + "NetworkSignal": "components.network_signal", + "NetworkTimelineEvent": "components.network_timeline_event", + "NetworksListMembersResponse": "networks.list_members_response", + "NetworksListResponse": "networks.list_response", + "NetworksListResponseKpis": "networks.list_response", + "NetworksListSignalsResponse": "networks.list_signals_response", + "NetworksNotesCreateParams": "networks.notes.create_params", + "NetworksNotesListResponse": "networks.notes.list_response", + "NetworksRetrieveTimelineResponse": "networks.retrieve_timeline_response", + "Organization": "components.organization", + "OrganizationSummary": "components.organization_summary", + "PassiveLivenessCheckParams": "passive_liveness.check_params", + "PassiveLivenessCheckResponse": "passive_liveness.check_response", + "PassiveLivenessCheckResponseLiveness": "passive_liveness.check_response", + "PassiveLivenessCheckResponseLivenessUserImage": "passive_liveness.check_response", + "PassiveLivenessCheckResponseLivenessUserImageEntities": "passive_liveness.check_response", + "PassiveLivenessCheckResponseLivenessWarnings": "passive_liveness.check_response", + "PhoneAssessRiskParams": "phone.assess_risk_params", + "PhoneCheckCodeParams": "phone.check_code_params", + "PhoneIntelligence": "components.phone_intelligence", + "PhoneMatchItem": "components.phone_match_item", + "PhoneRiskResponse": "components.phone_risk_response", + "PhoneRiskResponsePhone": "components.phone_risk_response", + "PhoneRiskResponsePhoneCarrier": "components.phone_risk_response", + "PhoneSendCodeParams": "phone.send_code_params", + "PhoneSendCodeParamsOptions": "phone.send_code_params", + "PhoneSendCodeParamsSignals": "phone.send_code_params", + "PhoneSocialFootprint": "components.phone_social_footprint", + "PhoneVerificationCheckResponse": "components.phone_verification_check_response", + "PhoneVerificationCheckResponsePhone": "components.phone_verification_check_response", + "PhoneVerificationCheckResponsePhoneCarrier": "components.phone_verification_check_response", + "PhoneVerificationCheckResponsePhoneLifecycle": "components.phone_verification_check_response", + "PhoneVerificationCheckResponsePhoneMatches": "components.phone_verification_check_response", + "PhoneVerificationCheckResponsePhoneWarnings": "components.phone_verification_check_response", + "PhoneVerificationSendResponse": "components.phone_verification_send_response", + "ProofOfAddressVerifyParams": "proof_of_address.verify_params", + "ProofOfAddressVerifyResponse": "proof_of_address.verify_response", + "ProofOfAddressVerifyResponsePoa": "proof_of_address.verify_response", + "ProofOfAddressVerifyResponsePoaExtraFields": "proof_of_address.verify_response", + "ProofOfAddressVerifyResponsePoaPoaParsedAddress": "proof_of_address.verify_response", + "ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation": "proof_of_address.verify_response", + "ProofOfAddressVerifyResponsePoaWarnings": "proof_of_address.verify_response", + "QuestionnaireDetail": "components.questionnaire_detail", + "QuestionnaireListItem": "components.questionnaire_list_item", + "QuestionnairesCreateParams": "questionnaires.create_params", + "QuestionnairesListResponse": "questionnaires.list_response", + "QuestionnairesUpdateParams": "questionnaires.update_params", + "RegisterResponse": "components.register_response", + "RegulatoryReportDetail": "components.regulatory_report_detail", + "RegulatoryReportDetailNarrative": "components.regulatory_report_detail", + "RegulatoryReportDetailValidationErrors": "components.regulatory_report_detail", + "RegulatoryReportsCreateParams": "regulatory_reports.create_params", + "RegulatoryReportsCreateParamsNarrative": "regulatory_reports.create_params", + "RegulatoryReportsListResponse": "regulatory_reports.list_response", + "RegulatoryReportsRetrieveDownloadUrlResponse": "regulatory_reports.retrieve_download_url_response", + "RegulatoryReportsUpdateParams": "regulatory_reports.update_params", + "RegulatoryReportsUpdateParamsNarrative": "regulatory_reports.update_params", + "RegulatoryReportsValidateResponse": "regulatory_reports.validate_response", + "RegulatoryReportsValidateResponseErrors": "regulatory_reports.validate_response", + "ReportTemplateDetail": "components.report_template_detail", + "ReportTemplatesCreateParams": "report_templates.create_params", + "ReportTemplatesListResponse": "report_templates.list_response", + "ReportTemplatesUpdateParams": "report_templates.update_params", + "ReviewItem": "components.review_item", + "RiskLifecycleEvent": "components.risk_lifecycle_event", + "RiskSignals": "components.risk_signals", + "RiskWarning": "components.risk_warning", + "RoutingPublicationCaseRoutingRule": "components.routing_publication_case_routing_rule", + "RoutingPublicationFieldEnum": "components.routing_publication_field_enum", + "RoutingPublicationOperatorEnum": "components.routing_publication_operator_enum", + "RoutingPublicationPaginatedPolicyRevisionList": "components.routing_publication_paginated_policy_revision_list", + "RoutingPublicationPolicyDraft": "components.routing_publication_policy_draft", + "RoutingPublicationPolicyDraftResponse": "components.routing_publication_policy_draft_response", + "RoutingPublicationPolicyRevision": "components.routing_publication_policy_revision", + "RoutingPublicationRoutingCondition": "components.routing_publication_routing_condition", + "SessionImportError": "components.session_import_error", + "SessionImportJob": "components.session_import_job", + "SessionsBatchDeleteParams": "sessions.batch_delete_params", + "SessionsBatchDeleteResponse": "sessions.batch_delete_response", + "SessionsBatchDeleteResponse202": "sessions.batch_delete_response", + "SessionsBatchDeleteResponse202Outcomes": "sessions.batch_delete_response", + "SessionsBatchDeleteResponseResults": "sessions.batch_delete_response", + "SessionsBulkUpdateAmlHitStatusParams": "sessions.bulk_update_aml_hit_status_params", + "SessionsBulkUpdateAmlHitStatusParamsHitUpdates": "sessions.bulk_update_aml_hit_status_params", + "SessionsCreateParams": "sessions.create_params", + "SessionsCreateParamsContactDetails": "sessions.create_params", + "SessionsCreateParamsExpectedDetails": "sessions.create_params", + "SessionsCreateResponse": "sessions.create_response", + "SessionsDeleteParams": "sessions.delete_params", + "SessionsDeleteResponse": "sessions.delete_response", + "SessionsImportSharedParams": "sessions.import_shared_params", + "SessionsImportsCreateParams": "sessions.imports.create_params", + "SessionsImportsListErrorsResponse": "sessions.imports.list_errors_response", + "SessionsListNetworksResponse": "sessions.list_networks_response", + "SessionsListResponse": "sessions.list_response", + "SessionsListResponseResultsKybSessionRowSessionKindBusiness": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUser": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserFeatures": "sessions.list_response", + "SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber": "sessions.list_response", + "SessionsPreviewResubmissionParams": "sessions.preview_resubmission_params", + "SessionsPreviewResubmissionParamsNodesToResubmit": "sessions.preview_resubmission_params", + "SessionsPreviewResubmissionResponse": "sessions.preview_resubmission_response", + "SessionsPreviewResubmissionResponseNodesToResubmit": "sessions.preview_resubmission_response", + "SessionsRetrieveDecisionResponse": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreenings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHits": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsScreenedData": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseAmlScreeningsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseContactDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDatabaseValidations": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDatabaseValidationsErrors": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDatabaseValidationsValidations": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDatabaseValidationsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentAiDocuments": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentAiDocumentsItems": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentVerifications": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentVerificationsItems": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseEmailVerifications": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseEmailVerificationsBreaches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseEmailVerificationsLifecycle": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseEmailVerificationsMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseEmailVerificationsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseExpectedDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseFaceMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseFaceMatchesWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerifications": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsExtraFields": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsIdLookup": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsMrz": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddress": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsWalletVerification": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIdVerificationsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalyses": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesIdDocument": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesIp": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesIpLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesPoaDocument": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseIpAnalysesWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecks": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseLivenessChecks": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseLivenessChecksMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseLivenessChecksWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseNfcVerifications": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseNfcVerificationsChipData": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseNfcVerificationsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePhoneVerifications": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePhoneVerificationsCarrier": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePhoneVerificationsMatches": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePhoneVerificationsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerifications": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsExtraFields": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponsePoaVerificationsWarnings": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseQuestionnaireResponses": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSections": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseRegistryChecks": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseRegistryChecksCompany": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseResubmissions": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseResubmissionsNodes": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseResubmissionsRequestedBy": "sessions.retrieve_decision_response", + "SessionsRetrieveDecisionResponseReviews": "sessions.retrieve_decision_response", + "SessionsRetrieveDeletionJobResponse": "sessions.retrieve_deletion_job_response", + "SessionsRetrieveDeletionJobResponseOutcomes": "sessions.retrieve_deletion_job_response", + "SessionsReviewsCreateParams": "sessions.reviews.create_params", + "SessionsReviewsCreateResponse": "sessions.reviews.create_response", + "SessionsReviewsListResponse": "sessions.reviews.list_response", + "SessionsShareParams": "sessions.share_params", + "SessionsShareResponse": "sessions.share_response", + "SessionsUpdateAmlHitStatusParams": "sessions.update_aml_hit_status_params", + "SessionsUpdateDataParams": "sessions.update_data_params", + "SessionsUpdateDataParamsParsedAddress": "sessions.update_data_params", + "SessionsUpdateDataResponse": "sessions.update_data_response", + "SessionsUpdateDataResponseParsedAddress": "sessions.update_data_response", + "SessionsUpdateFeatureStatusParams": "sessions.update_feature_status_params", + "SessionsUpdateFeatureStatusResponse": "sessions.update_feature_status_response", + "SessionsUpdateKybCompanyDataParams": "sessions.update_kyb_company_data_params", + "SessionsUpdateKybCompanyDataResponse": "sessions.update_kyb_company_data_response", + "SessionsUpdateKybPartyParams": "sessions.update_kyb_party_params", + "SessionsUpdateKybPartyResponse": "sessions.update_kyb_party_response", + "SessionsUpdatePoaDataParams": "sessions.update_poa_data_params", + "SessionsUpdatePoaDataParamsPoaParsedAddress": "sessions.update_poa_data_params", + "SessionsUpdatePoaDataResponse": "sessions.update_poa_data_response", + "SessionsUpdatePoaDataResponsePoaParsedAddress": "sessions.update_poa_data_response", + "SessionsUpdateStatusParams": "sessions.update_status_params", + "SessionsUpdateStatusParamsNodesToResubmit": "sessions.update_status_params", + "SessionsUpdateStatusResponse": "sessions.update_status_response", + "SimpleQuestionnaireElement": "components.simple_questionnaire_element", + "SimpleQuestionnaireElementOptions": "components.simple_questionnaire_element", + "SimpleQuestionnaireElementValidation": "components.simple_questionnaire_element", + "SimpleWorkflowFeature": "components.simple_workflow_feature", + "SocialFootprintCategorySummary": "components.social_footprint_category_summary", + "SocialFootprintPlatform": "components.social_footprint_platform", + "SocialFootprintProfile": "components.social_footprint_profile", + "TransactionDetail": "components.transaction_detail", + "TransactionDetailActionRequiredVerificationSession": "components.transaction_detail", + "TransactionDetailActionRequiredWalletOwnership": "components.transaction_detail", + "TransactionDetailActivities": "components.transaction_detail", + "TransactionDetailAlerts": "components.transaction_detail", + "TransactionDetailDocuments": "components.transaction_detail", + "TransactionDetailDocumentsFiles": "components.transaction_detail", + "TransactionDetailNetworkSnapshot": "components.transaction_detail", + "TransactionDetailParties": "components.transaction_detail", + "TransactionDetailPartiesDevice": "components.transaction_detail", + "TransactionDetailPartiesDeviceNetworkContext": "components.transaction_detail", + "TransactionDetailPaymentMethods": "components.transaction_detail", + "TransactionDetailProviderResults": "components.transaction_detail", + "TransactionDetailRemediation": "components.transaction_detail", + "TransactionDetailRuleRuns": "components.transaction_detail", + "TransactionDetailTags": "components.transaction_detail", + "TransactionDetailTravelRule": "components.transaction_detail", + "TransactionRuleAction": "components.transaction_rule_action", + "TransactionRuleActionParam": "components.transaction_rule_action", + "TransactionRuleActionVariant1": "components.transaction_rule_action", + "TransactionRuleActionVariant1Param": "components.transaction_rule_action", + "TransactionRuleActionVariant2": "components.transaction_rule_action", + "TransactionRuleActionVariant2Param": "components.transaction_rule_action", + "TransactionRuleActionVariant3": "components.transaction_rule_action", + "TransactionRuleActionVariant3Param": "components.transaction_rule_action", + "TransactionRuleActionVariant4": "components.transaction_rule_action", + "TransactionRuleActionVariant4Param": "components.transaction_rule_action", + "TransactionRuleActionVariant5": "components.transaction_rule_action", + "TransactionRuleActionVariant5Param": "components.transaction_rule_action", + "TransactionRuleActionVariant6": "components.transaction_rule_action", + "TransactionRuleActionVariant6Param": "components.transaction_rule_action", + "TransactionRuleAggregation": "components.transaction_rule_aggregation", + "TransactionRuleAggregationParam": "components.transaction_rule_aggregation", + "TransactionRuleBacktestResult": "components.transaction_rule_backtest_result", + "TransactionRuleCondition": "components.transaction_rule_condition", + "TransactionRuleConditionParam": "components.transaction_rule_condition", + "TransactionRuleDetail": "components.transaction_rule_detail", + "TransactionRuleInstallResult": "components.transaction_rule_install_result", + "TransactionRuleLibraryEntry": "components.transaction_rule_library_entry", + "TransactionRuleScope": "components.transaction_rule_scope", + "TransactionRuleScopeParam": "components.transaction_rule_scope", + "TransactionsCreateParams": "transactions.create_params", + "TransactionsCreateParamsCounterparty": "transactions.create_params", + "TransactionsCreateParamsCounterpartyDeviceContext": "transactions.create_params", + "TransactionsCreateParamsCounterpartyDeviceContextNetworkContext": "transactions.create_params", + "TransactionsCreateParamsCounterpartyPaymentMethod": "transactions.create_params", + "TransactionsCreateParamsNetworkSnapshot": "transactions.create_params", + "TransactionsCreateParamsSubject": "transactions.create_params", + "TransactionsCreateParamsSubjectAddress": "transactions.create_params", + "TransactionsCreateParamsSubjectDeviceContext": "transactions.create_params", + "TransactionsCreateParamsSubjectDeviceContextNetworkContext": "transactions.create_params", + "TransactionsCreateParamsSubjectInstitutionDetails": "transactions.create_params", + "TransactionsCreateParamsSubjectPaymentMethod": "transactions.create_params", + "TransactionsCreateParamsTransactionDetails": "transactions.create_params", + "TransactionsCreateParamsTransactionDetailsCryptoParams": "transactions.create_params", + "TransactionsCreateParamsTravelRuleDetails": "transactions.create_params", + "TransactionsCreateSdkTokenParams": "transactions.create_sdk_token_params", + "TransactionsCreateSdkTokenResponse": "transactions.create_sdk_token_response", + "TransactionsListNetworksResponse": "transactions.list_networks_response", + "TransactionsListPartyNetworksResponse": "transactions.list_party_networks_response", + "TransactionsListResponse": "transactions.list_response", + "TransactionsListResponseResults": "transactions.list_response", + "TransactionsListResponseResultsTags": "transactions.list_response", + "TransactionsRulesBacktestParams": "transactions.rules.backtest_params", + "TransactionsRulesCreateParams": "transactions.rules.create_params", + "TransactionsRulesInstallLibraryParams": "transactions.rules.install_library_params", + "TransactionsRulesListLibraryResponse": "transactions.rules.list_library_response", + "TransactionsRulesListResponse": "transactions.rules.list_response", + "TransactionsRulesUninstallLibraryParams": "transactions.rules.uninstall_library_params", + "TransactionsRulesUpdateParams": "transactions.rules.update_params", + "TransactionsTravelRuleConfirmOwnershipParams": "transactions.travel_rule.confirm_ownership_params", + "TransactionsTravelRuleUpdateTransferParams": "transactions.travel_rule.update_transfer_params", + "TravelRuleCreateWidgetSessionParams": "travel_rule.create_widget_session_params", + "TravelRuleInboundResponse": "components.travel_rule_inbound_response", + "TravelRuleListVaspsResponse": "travel_rule.list_vasps_response", + "TravelRuleRegisterInboundParams": "travel_rule.register_inbound_params", + "TravelRuleRegisterInboundParamsOriginatingVasp": "travel_rule.register_inbound_params", + "TravelRuleSettingsDetail": "components.travel_rule_settings_detail", + "TravelRuleSettingsDetailNetworks": "components.travel_rule_settings_detail", + "TravelRuleSettingsDetailNetworksGtr": "components.travel_rule_settings_detail", + "TravelRuleSettingsDetailNetworksSygna": "components.travel_rule_settings_detail", + "TravelRuleSettingsDetailNetworksTrust": "components.travel_rule_settings_detail", + "TravelRuleSettingsDetailNetworksVerifyvasp": "components.travel_rule_settings_detail", + "TravelRuleTransferDetail": "components.travel_rule_transfer_detail", + "TravelRuleTransferDetailCounterpartyVasp": "components.travel_rule_transfer_detail", + "TravelRuleTransferDetailTimeline": "components.travel_rule_transfer_detail", + "TravelRuleUpdateSettingsParams": "travel_rule.update_settings_params", + "TravelRuleWalletAddressesCreateParams": "travel_rule.wallet_addresses.create_params", + "TravelRuleWalletAddressesListResponse": "travel_rule.wallet_addresses.list_response", + "TravelRuleWalletAddressesUpdateParams": "travel_rule.wallet_addresses.update_params", + "TravelRuleWidgetSessionResponse": "components.travel_rule_widget_session_response", + "UserDetailItem": "components.user_detail_item", + "UserDetailItemComments": "components.user_detail_item", + "UserListItem": "components.user_list_item", + "UserListItemFeaturesList": "components.user_list_item", + "UserListItemTags": "components.user_list_item", + "UsersBatchDeleteParams": "users.batch_delete_params", + "UsersBatchDeleteResponse": "users.batch_delete_response", + "UsersCreateParams": "users.create_params", + "UsersListNetworksResponse": "users.list_networks_response", + "UsersListResponse": "users.list_response", + "UsersRetrieveOngoingMonitoringJobResponse": "users.retrieve_ongoing_monitoring_job_response", + "UsersRetrieveOngoingMonitoringJobResponseEstimate": "users.retrieve_ongoing_monitoring_job_response", + "UsersRetrieveOngoingMonitoringJobResponseOutcomes": "users.retrieve_ongoing_monitoring_job_response", + "UsersSetAmlMonitoringParams": "users.set_aml_monitoring_params", + "UsersSetAmlMonitoringResponse202": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponse202Estimate": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponse202Outcomes": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponse202Results": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponseVariant1": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponseVariant2": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponseVariant2Estimate": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponseVariant2Outcomes": "users.set_aml_monitoring_response", + "UsersSetAmlMonitoringResponseVariant2Results": "users.set_aml_monitoring_response", + "UsersSetOngoingMonitoringParams": "users.set_ongoing_monitoring_params", + "UsersSetOngoingMonitoringResponse202": "users.set_ongoing_monitoring_response", + "UsersSetOngoingMonitoringResponse202Estimate": "users.set_ongoing_monitoring_response", + "UsersSetOngoingMonitoringResponse202Outcomes": "users.set_ongoing_monitoring_response", + "UsersSetOngoingMonitoringResponseVariant1": "users.set_ongoing_monitoring_response", + "UsersSetOngoingMonitoringResponseVariant2": "users.set_ongoing_monitoring_response", + "UsersSetOngoingMonitoringResponseVariant2Estimate": "users.set_ongoing_monitoring_response", + "UsersSetOngoingMonitoringResponseVariant2Outcomes": "users.set_ongoing_monitoring_response", + "UsersUpdateParams": "users.update_params", + "UsersUpdateStatusParams": "users.update_status_params", + "UsersUploadFaceParams": "users.upload_face_params", + "UsersUploadFaceResponse": "users.upload_face_response", + "VaspDirectoryEntry": "components.vasp_directory_entry", + "VerifyEmailResponse": "components.verify_email_response", + "WalletAddressEntry": "components.wallet_address_entry", + "WalletAddressEntryUpdate": "components.wallet_address_entry_update", + "WalletScreeningResult": "components.wallet_screening_result", + "WalletScreeningResultCounterpartyConnections": "components.wallet_screening_result", + "WalletScreeningResultDestinationOfFunds": "components.wallet_screening_result", + "WalletScreeningResultRiskFactors": "components.wallet_screening_result", + "WalletScreeningResultSourceOfFunds": "components.wallet_screening_result", + "WalletScreeningScreenParams": "wallet_screening.screen_params", + "WalletScreeningScreenPdfParams": "wallet_screening.screen_pdf_params", + "WalletScreeningScreenPdfParamsCounterpartyConnections": "wallet_screening.screen_pdf_params", + "WalletScreeningScreenPdfParamsDestinationOfFunds": "wallet_screening.screen_pdf_params", + "WalletScreeningScreenPdfParamsRiskFactors": "wallet_screening.screen_pdf_params", + "WalletScreeningScreenPdfParamsSourceOfFunds": "wallet_screening.screen_pdf_params", + "WebhookEvent": "components.webhook_event", + "WebhookSubscribedEvents": "components.webhook_subscribed_events", + "WebhookSubscribedEventsParam": "components.webhook_subscribed_events", + "WebhooksDestinationsCreateParams": "webhooks.destinations.create_params", + "WebhooksDestinationsCreateResponse": "webhooks.destinations.create_response", + "WebhooksDestinationsCreateResponseSummary": "webhooks.destinations.create_response", + "WebhooksDestinationsListResponseItem": "webhooks.destinations.list_response", + "WebhooksDestinationsRetrieveResponse": "webhooks.destinations.retrieve_response", + "WebhooksDestinationsRetrieveResponseSummary": "webhooks.destinations.retrieve_response", + "WebhooksDestinationsUpdateParams": "webhooks.destinations.update_params", + "WebhooksDestinationsUpdateResponse": "webhooks.destinations.update_response", + "WebhooksDestinationsUpdateResponseSummary": "webhooks.destinations.update_response", + "WorkflowListItem": "components.workflow_list_item", + "WorkflowListItemCreatedBy": "components.workflow_list_item", + "WorkflowsCreateParams": "workflows.create_params", + "WorkflowsListResponse": "workflows.list_response", + "WorkflowsUpdateParams": "workflows.update_params", +} + +__all__ = [ + "AccountApplicationsCreateParams", + "AccountApplicationsUpdateParams", + "AccountLoginParams", + "AccountRegisterParams", + "AccountResendOtpParams", + "AccountVerifyEmailParams", + "AgeEstimationEstimateParams", + "AgeEstimationEstimateResponse", + "AgeEstimationEstimateResponseAgeEstimation", + "AgeEstimationEstimateResponseAgeEstimationUserImage", + "AgeEstimationEstimateResponseAgeEstimationUserImageEntities", + "AgeEstimationEstimateResponseAgeEstimationWarnings", + "AmlMonitoringRetrieveJobResponse", + "AmlMonitoringRetrieveJobResponseOutcomes", + "AmlMonitoringRetrieveJobResponseResults", + "AmlScreenParams", + "AmlScreenResponse", + "AmlScreenResponseAml", + "AmlScreenResponseAmlHits", + "AmlScreenResponseAmlHitsAdverseMediaMatches", + "AmlScreenResponseAmlHitsLinkedEntities", + "AmlScreenResponseAmlHitsPepMatches", + "AmlScreenResponseAmlHitsScoreBreakdown", + "AmlScreenResponseAmlScreenedData", + "AmlScreenResponseAmlWarnings", + "Application", + "ApplicationCredentialsSummary", + "BillingRetrieveBalanceResponse", + "BillingTopUpParams", + "BillingTopUpResponse", + "BiometricTemplatesBatchDeleteParams", + "BiometricTemplatesBatchDeleteResponse", + "BiometricTemplatesBatchDeleteResponseResults", + "BiometricTemplatesCountResponse", + "BiometricTemplatesDeleteResponse", + "BiometricTemplatesListResponse", + "BiometricTemplatesListResponseResults", + "BiometricTemplatesRetrieveResponse", + "BrandingCustomization", + "BusinessDetailItem", + "BusinessListItem", + "BusinessListItemFeaturesList", + "BusinessListItemTags", + "BusinessSessionsListNetworksResponse", + "BusinessesBatchDeleteParams", + "BusinessesBatchDeleteResponse", + "BusinessesCreateParams", + "BusinessesListNetworksResponse", + "BusinessesListResponse", + "BusinessesRetrieveOngoingMonitoringJobResponse", + "BusinessesRetrieveOngoingMonitoringJobResponseEstimate", + "BusinessesRetrieveOngoingMonitoringJobResponseOutcomes", + "BusinessesRetrieveSessionOngoingMonitoringJobResponse", + "BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate", + "BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes", + "BusinessesSetAmlMonitoringParams", + "BusinessesSetAmlMonitoringResponse202", + "BusinessesSetAmlMonitoringResponse202Estimate", + "BusinessesSetAmlMonitoringResponse202Outcomes", + "BusinessesSetAmlMonitoringResponse202Results", + "BusinessesSetAmlMonitoringResponseVariant1", + "BusinessesSetAmlMonitoringResponseVariant2", + "BusinessesSetAmlMonitoringResponseVariant2Estimate", + "BusinessesSetAmlMonitoringResponseVariant2Outcomes", + "BusinessesSetAmlMonitoringResponseVariant2Results", + "BusinessesSetOngoingMonitoringParams", + "BusinessesSetOngoingMonitoringResponse202", + "BusinessesSetOngoingMonitoringResponse202Estimate", + "BusinessesSetOngoingMonitoringResponse202Outcomes", + "BusinessesSetOngoingMonitoringResponseVariant1", + "BusinessesSetOngoingMonitoringResponseVariant2", + "BusinessesSetOngoingMonitoringResponseVariant2Estimate", + "BusinessesSetOngoingMonitoringResponseVariant2Outcomes", + "BusinessesSetSessionOngoingMonitoringParams", + "BusinessesSetSessionOngoingMonitoringResponse202", + "BusinessesSetSessionOngoingMonitoringResponse202Estimate", + "BusinessesSetSessionOngoingMonitoringResponse202Outcomes", + "BusinessesSetSessionOngoingMonitoringResponseVariant1", + "BusinessesSetSessionOngoingMonitoringResponseVariant2", + "BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate", + "BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes", + "BusinessesUpdateParams", + "BusinessesUpdateStatusParams", + "CaseActionActionEnum", + "CaseActionCaseActionReceipt", + "CaseActionCaseActionReceiptPage", + "CaseActionCaseEntityActionResult", + "CaseActionEntityTypeEnum", + "CaseActionStatusEnum", + "CaseAssignee", + "CaseBlueprintDetail", + "CaseBlueprintDetailContentConfig", + "CaseBlueprintDetailContentConfigAmlControl", + "CaseBlueprintDetailContentConfigChecklist", + "CaseBlueprintDetailContentConfigChecklistItems", + "CaseBlueprintDetailContentConfigFinancial", + "CaseBlueprintDetailContentConfigIdentity", + "CaseBlueprintDetailContentConfigReporting", + "CaseBlueprintDetailContentConfigVerifications", + "CaseBlueprintDetailCreationSources", + "CaseBlueprintDetailReceivesEscalationFrom", + "CaseBlueprintListItem", + "CaseBlueprintSummary", + "CaseChecklistItem", + "CaseCollaborationCaseNote", + "CaseCollaborationCaseNoteReply", + "CaseCollaborationPaginatedCaseNoteList", + "CaseContentConfig", + "CaseContentConfigAmlControl", + "CaseContentConfigAmlControlParam", + "CaseContentConfigChecklist", + "CaseContentConfigChecklistItems", + "CaseContentConfigChecklistItemsParam", + "CaseContentConfigChecklistParam", + "CaseContentConfigFinancial", + "CaseContentConfigFinancialParam", + "CaseContentConfigIdentity", + "CaseContentConfigIdentityParam", + "CaseContentConfigParam", + "CaseContentConfigReporting", + "CaseContentConfigReportingParam", + "CaseContentConfigVerifications", + "CaseContentConfigVerificationsParam", + "CaseDetail", + "CaseDetailPendingResolution", + "CaseDuplicateCaseDuplicate", + "CaseDuplicateCaseDuplicatePage", + "CaseDuplicateCaseFraudSourceContextRequest", + "CaseDuplicateCaseLinkCreateRequest", + "CaseDuplicateCaseSubjectInputRequest", + "CaseDuplicateEntityTypeEnum", + "CaseDuplicatePriorityEnum", + "CaseDuplicateStatusEnum", + "CaseDuplicateTypeEnum", + "CaseEvent", + "CaseLink", + "CaseListItem", + "CaseNote", + "CaseNoteAttachments", + "CasePublicationAssignmentMethodEnum", + "CasePublicationCaseBlueprint", + "CasePublicationPaginatedPolicyRevisionList", + "CasePublicationPolicyDraft", + "CasePublicationPolicyDraftResponse", + "CasePublicationPolicyRevision", + "CaseReviewTeam", + "CaseReviewTeamMember", + "CaseReviewTeamMemberParam", + "CaseRoutingCondition", + "CaseRoutingConditionParam", + "CaseRoutingPreview", + "CaseRoutingPreviewCandidates", + "CaseRoutingPreviewMatchedRules", + "CaseRoutingRule", + "CaseRoutingRuleCreate", + "CaseSubject", + "CasesApplyEntityActionParams", + "CasesAssignParams", + "CasesBlueprintsCreateParams", + "CasesBlueprintsDiscardDraftParams", + "CasesBlueprintsInstallPresetsParams", + "CasesBlueprintsInstallPresetsResponse", + "CasesBlueprintsListPresetsResponse", + "CasesBlueprintsListPresetsResponseResults", + "CasesBlueprintsListResponse", + "CasesBlueprintsPublishParams", + "CasesBlueprintsSaveDraftParams", + "CasesBlueprintsUpdateParams", + "CasesBulkAssignParams", + "CasesBulkAssignResponse", + "CasesBulkAssignResponseFailures", + "CasesBulkUpdateStatusParams", + "CasesBulkUpdateStatusResponse", + "CasesBulkUpdateStatusResponseFailures", + "CasesChecklistToggleItemParams", + "CasesCreateParams", + "CasesCreateParamsSubject", + "CasesFindDuplicatesParams", + "CasesLinksCreateParams", + "CasesListEventsResponse", + "CasesListResponse", + "CasesListReviewersResponse", + "CasesListReviewersResponseResults", + "CasesNotesCreateAttachmentUploadParams", + "CasesNotesCreateAttachmentUploadResponse", + "CasesNotesCreateParams", + "CasesNotesRetrieveAttachmentUrlResponse", + "CasesNotesUpdateParams", + "CasesPrintResponse", + "CasesRegulatoryReportsCreateParams", + "CasesRegulatoryReportsCreateParamsNarrative", + "CasesRegulatoryReportsListResponse", + "CasesRejectResolutionParams", + "CasesRequestsCreateParams", + "CasesRequestsPreviewParams", + "CasesResolveParams", + "CasesRetrieveStatisticsResponse", + "CasesRetrieveStatisticsResponseQueue", + "CasesRetrieveStatisticsResponseSeries", + "CasesRetrieveStatisticsResponseTables", + "CasesRetrieveStatisticsResponseTablesBlueprint", + "CasesRetrieveStatisticsResponseTablesOfficer", + "CasesRetrieveStatisticsResponseTablesSource", + "CasesRetrieveStatisticsResponseTablesStatus", + "CasesRetrieveStatisticsResponseTotals", + "CasesRoutingRulesCreateParams", + "CasesRoutingRulesDiscardDraftParams", + "CasesRoutingRulesListResponse", + "CasesRoutingRulesPreviewParams", + "CasesRoutingRulesPublishParams", + "CasesRoutingRulesSaveDraftParams", + "CasesRoutingRulesUpdateParams", + "CasesTeamsCreateParams", + "CasesTeamsListResponse", + "CasesTeamsUpdateParams", + "CasesTransferParams", + "CasesUpdateParams", + "CustomerMessagingCaseRequest", + "CustomerMessagingCaseRequestContact", + "CustomerMessagingCaseRequestOptions", + "CustomerMessagingCaseRequestPreviewResponse", + "CustomerMessagingLanguageEnum", + "CustomerMessagingLanguagesEnum", + "CustomerMessagingPaginatedCaseRequestList", + "CustomerMessagingPurposeEnum", + "CustomerMessagingPurposesEnum", + "CustomizationUpdateParams", + "DatabaseValidationValidateParams", + "DatabaseValidationValidateParamsAddressVariant1", + "DatabaseValidationValidateResponse", + "DatabaseValidationValidateResponseDatabaseValidation", + "DatabaseValidationValidateResponseDatabaseValidationErrors", + "DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1", + "DatabaseValidationValidateResponseDatabaseValidationWarnings", + "DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum", + "DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum", + "DigitalIdWalletApiCountry520Enum", + "DigitalIdWalletApiCountryAfcEnum", + "DigitalIdWalletApiCountryE2dEnum", + "DigitalIdWalletApiCountryEe9Enum", + "DigitalIdWalletApiDeviceEnum", + "DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum", + "DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum", + "DigitalIdWalletApiEudiWalletVerificationStartCountryEnum", + "DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum", + "DigitalIdWalletApiFtnWalletVerificationStartCountryEnum", + "DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum", + "DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum", + "DigitalIdWalletApiIdinWalletVerificationStartCountryEnum", + "DigitalIdWalletApiInteractionEnum", + "DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum", + "DigitalIdWalletApiMitidWalletVerificationStartCountryEnum", + "DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum", + "DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum", + "DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum", + "DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum", + "DigitalIdWalletApiWallet", + "DigitalIdWalletApiWalletCatalogEntry", + "DigitalIdWalletApiWalletCatalogResponse", + "DigitalIdWalletApiWalletIdentity", + "DigitalIdWalletApiWalletNextAction", + "DigitalIdWalletApiWalletNextActionTypeEnum", + "DigitalIdWalletApiWalletVerificationResponse", + "DigitalIdWalletApiWalletVerificationResult", + "DigitalIdWalletApiWalletVerificationResultOutcomeEnum", + "DocumentAIDetectedCode", + "DocumentAiAnalyzeParams", + "DocumentAiAnalyzeResponse", + "DocumentAiAnalyzeResponseDocumentAi", + "DocumentAiAnalyzeResponseDocumentAiFields", + "DocumentAiAnalyzeResponseDocumentAiWarnings", + "EmailAssessRiskParams", + "EmailCheckCodeParams", + "EmailIntelligence", + "EmailMatchItem", + "EmailRiskResponse", + "EmailRiskResponseEmail", + "EmailRiskResponseEmailBreaches", + "EmailSendCodeParams", + "EmailSendCodeParamsOptions", + "EmailSendCodeParamsSignals", + "EmailSocialFootprint", + "EmailVerificationCheckResponse", + "EmailVerificationCheckResponseEmail", + "EmailVerificationCheckResponseEmailBreaches", + "EmailVerificationCheckResponseEmailLifecycle", + "EmailVerificationCheckResponseEmailMatches", + "EmailVerificationCheckResponseEmailWarnings", + "EmailVerificationSendResponse", + "EntityNetworkMembership", + "FaceMatchCompareParams", + "FaceMatchCompareResponse", + "FaceMatchCompareResponseFaceMatch", + "FaceMatchCompareResponseFaceMatchRefImage", + "FaceMatchCompareResponseFaceMatchRefImageEntities", + "FaceMatchCompareResponseFaceMatchUserImage", + "FaceMatchCompareResponseFaceMatchUserImageEntities", + "FaceMatchCompareResponseFaceMatchWarnings", + "FaceSearchSearchParams", + "FaceSearchSearchResponse", + "FaceSearchSearchResponseFaceSearch", + "FaceSearchSearchResponseFaceSearchMatches", + "FaceSearchSearchResponseFaceSearchMatchesUserDetails", + "FaceSearchSearchResponseFaceSearchUserImage", + "FaceSearchSearchResponseFaceSearchUserImageEntities", + "FaceSearchSearchResponseFaceSearchWarnings", + "FraudAvailabilityEnum", + "FraudBandEnum", + "FraudCheckAttributesRequest", + "FraudCheckContextRequest", + "FraudCheckIdentifierRequest", + "FraudCheckIdentityRelationship", + "FraudCheckInput", + "FraudCheckModuleReport", + "FraudCheckModuleSelectionRequest", + "FraudCheckPlan", + "FraudCheckPlannedModule", + "FraudCheckResponse", + "FraudCheckRisk", + "FraudCheckSubjectRequest", + "FraudChecksCreateParams", + "FraudChecksPlanParams", + "FraudDimensionEnum", + "FraudExcludeEnum", + "FraudNextActionEnum", + "FraudProcessingStatusEnum", + "FraudRecommendationEnum", + "FraudResultEnum", + "FraudStatusEnum", + "FraudTypeEnum", + "IPRiskResponse", + "IPRiskResponseIp", + "IdVerificationVerifyParams", + "IdVerificationVerifyResponse", + "IdVerificationVerifyResponseIdVerification", + "IdVerificationVerifyResponseIdVerificationBackImageQualityScore", + "IdVerificationVerifyResponseIdVerificationBarcodes", + "IdVerificationVerifyResponseIdVerificationFrontImageQualityScore", + "IdVerificationVerifyResponseIdVerificationMrz", + "IdVerificationVerifyResponseIdVerificationParsedAddress", + "IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation", + "IdVerificationVerifyResponseIdVerificationWarnings", + "IdWalletsStartAudkenniParams", + "IdWalletsStartBankidCzParams", + "IdWalletsStartBankidNoParams", + "IdWalletsStartBankidSeParams", + "IdWalletsStartBuypassParams", + "IdWalletsStartConnectidParams", + "IdWalletsStartDiiaParams", + "IdWalletsStartEparakstsMobileParams", + "IdWalletsStartEudiParams", + "IdWalletsStartFranceconnectParams", + "IdWalletsStartFrejaidParams", + "IdWalletsStartFtnParams", + "IdWalletsStartGovbrParams", + "IdWalletsStartGovukWalletParams", + "IdWalletsStartIdcardEeParams", + "IdWalletsStartIdinParams", + "IdWalletsStartItsmeParams", + "IdWalletsStartMitidParams", + "IdWalletsStartMobileIdParams", + "IdWalletsStartMojeidParams", + "IdWalletsStartOneidParams", + "IdWalletsStartPersonalausweisParams", + "IdWalletsStartSmartIdParams", + "IdWalletsStartUaePassParams", + "IdWalletsStartVippsParams", + "IpAssessRiskParams", + "KYBRegistryAddress", + "KYBRegistryIndustry", + "KYBRegistryOwnership", + "KYBRegistryOwnershipUnresolved", + "KYBRegistryShareholding", + "KybListShareholdersParams", + "KybListShareholdersResponse", + "KybListShareholdersResponse202", + "KybListShareholdersResponse202KybRegistry", + "KybListShareholdersResponse202KybRegistryBeneficialOwners", + "KybListShareholdersResponse202KybRegistryOfficers", + "KybListShareholdersResponseKybRegistry", + "KybListShareholdersResponseKybRegistryBeneficialOwners", + "KybListShareholdersResponseKybRegistryOfficers", + "KybListUbosParams", + "KybListUbosResponse", + "KybListUbosResponse202", + "KybListUbosResponse202KybRegistry", + "KybListUbosResponse202KybRegistryBeneficialOwners", + "KybListUbosResponse202KybRegistryOfficers", + "KybListUbosResponseKybRegistry", + "KybListUbosResponseKybRegistryBeneficialOwners", + "KybListUbosResponseKybRegistryOfficers", + "KybSearchParams", + "KybSearchResponse", + "KybSearchResponseKybRegistry", + "KybSearchResponseKybRegistryCompanies", + "KybSearchResponseKybRegistryPagination", + "KybSelectParams", + "KybSelectResponse", + "KybSelectResponseKybRegistry", + "KybSelectResponseKybRegistryBeneficialOwners", + "KybSelectResponseKybRegistryOfficers", + "LoginResponse", + "NetworkDetail", + "NetworkGraph", + "NetworkGraphEdge", + "NetworkGraphMetrics", + "NetworkGraphNode", + "NetworkListItem", + "NetworkMap", + "NetworkMapPoints", + "NetworkMember", + "NetworkMemberProvenance", + "NetworkNodeTag", + "NetworkNote", + "NetworkRiskDisplay", + "NetworkSignal", + "NetworkTimelineEvent", + "NetworksListMembersResponse", + "NetworksListResponse", + "NetworksListResponseKpis", + "NetworksListSignalsResponse", + "NetworksNotesCreateParams", + "NetworksNotesListResponse", + "NetworksRetrieveTimelineResponse", + "Organization", + "OrganizationSummary", + "PassiveLivenessCheckParams", + "PassiveLivenessCheckResponse", + "PassiveLivenessCheckResponseLiveness", + "PassiveLivenessCheckResponseLivenessUserImage", + "PassiveLivenessCheckResponseLivenessUserImageEntities", + "PassiveLivenessCheckResponseLivenessWarnings", + "PhoneAssessRiskParams", + "PhoneCheckCodeParams", + "PhoneIntelligence", + "PhoneMatchItem", + "PhoneRiskResponse", + "PhoneRiskResponsePhone", + "PhoneRiskResponsePhoneCarrier", + "PhoneSendCodeParams", + "PhoneSendCodeParamsOptions", + "PhoneSendCodeParamsSignals", + "PhoneSocialFootprint", + "PhoneVerificationCheckResponse", + "PhoneVerificationCheckResponsePhone", + "PhoneVerificationCheckResponsePhoneCarrier", + "PhoneVerificationCheckResponsePhoneLifecycle", + "PhoneVerificationCheckResponsePhoneMatches", + "PhoneVerificationCheckResponsePhoneWarnings", + "PhoneVerificationSendResponse", + "ProofOfAddressVerifyParams", + "ProofOfAddressVerifyResponse", + "ProofOfAddressVerifyResponsePoa", + "ProofOfAddressVerifyResponsePoaExtraFields", + "ProofOfAddressVerifyResponsePoaPoaParsedAddress", + "ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation", + "ProofOfAddressVerifyResponsePoaWarnings", + "QuestionnaireDetail", + "QuestionnaireListItem", + "QuestionnairesCreateParams", + "QuestionnairesListResponse", + "QuestionnairesUpdateParams", + "RegisterResponse", + "RegulatoryReportDetail", + "RegulatoryReportDetailNarrative", + "RegulatoryReportDetailValidationErrors", + "RegulatoryReportsCreateParams", + "RegulatoryReportsCreateParamsNarrative", + "RegulatoryReportsListResponse", + "RegulatoryReportsRetrieveDownloadUrlResponse", + "RegulatoryReportsUpdateParams", + "RegulatoryReportsUpdateParamsNarrative", + "RegulatoryReportsValidateResponse", + "RegulatoryReportsValidateResponseErrors", + "ReportTemplateDetail", + "ReportTemplatesCreateParams", + "ReportTemplatesListResponse", + "ReportTemplatesUpdateParams", + "ReviewItem", + "RiskLifecycleEvent", + "RiskSignals", + "RiskWarning", + "RoutingPublicationCaseRoutingRule", + "RoutingPublicationFieldEnum", + "RoutingPublicationOperatorEnum", + "RoutingPublicationPaginatedPolicyRevisionList", + "RoutingPublicationPolicyDraft", + "RoutingPublicationPolicyDraftResponse", + "RoutingPublicationPolicyRevision", + "RoutingPublicationRoutingCondition", + "SessionImportError", + "SessionImportJob", + "SessionsBatchDeleteParams", + "SessionsBatchDeleteResponse", + "SessionsBatchDeleteResponse202", + "SessionsBatchDeleteResponse202Outcomes", + "SessionsBatchDeleteResponseResults", + "SessionsBulkUpdateAmlHitStatusParams", + "SessionsBulkUpdateAmlHitStatusParamsHitUpdates", + "SessionsCreateParams", + "SessionsCreateParamsContactDetails", + "SessionsCreateParamsExpectedDetails", + "SessionsCreateResponse", + "SessionsDeleteParams", + "SessionsDeleteResponse", + "SessionsImportSharedParams", + "SessionsImportsCreateParams", + "SessionsImportsListErrorsResponse", + "SessionsListNetworksResponse", + "SessionsListResponse", + "SessionsListResponseResultsKybSessionRowSessionKindBusiness", + "SessionsListResponseResultsKycSessionRowSessionKindUser", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa", + "SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress", + "SessionsListResponseResultsKycSessionRowSessionKindUserFeatures", + "SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber", + "SessionsPreviewResubmissionParams", + "SessionsPreviewResubmissionParamsNodesToResubmit", + "SessionsPreviewResubmissionResponse", + "SessionsPreviewResubmissionResponseNodesToResubmit", + "SessionsRetrieveDecisionResponse", + "SessionsRetrieveDecisionResponseAmlScreenings", + "SessionsRetrieveDecisionResponseAmlScreeningsHits", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails", + "SessionsRetrieveDecisionResponseAmlScreeningsScreenedData", + "SessionsRetrieveDecisionResponseAmlScreeningsWarnings", + "SessionsRetrieveDecisionResponseContactDetails", + "SessionsRetrieveDecisionResponseDatabaseValidations", + "SessionsRetrieveDecisionResponseDatabaseValidationsErrors", + "SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData", + "SessionsRetrieveDecisionResponseDatabaseValidationsValidations", + "SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation", + "SessionsRetrieveDecisionResponseDatabaseValidationsWarnings", + "SessionsRetrieveDecisionResponseDocumentAiDocuments", + "SessionsRetrieveDecisionResponseDocumentAiDocumentsItems", + "SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields", + "SessionsRetrieveDecisionResponseDocumentVerifications", + "SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue", + "SessionsRetrieveDecisionResponseDocumentVerificationsItems", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionsRetrieveDecisionResponseEmailVerifications", + "SessionsRetrieveDecisionResponseEmailVerificationsBreaches", + "SessionsRetrieveDecisionResponseEmailVerificationsLifecycle", + "SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails", + "SessionsRetrieveDecisionResponseEmailVerificationsMatches", + "SessionsRetrieveDecisionResponseEmailVerificationsWarnings", + "SessionsRetrieveDecisionResponseExpectedDetails", + "SessionsRetrieveDecisionResponseFaceMatches", + "SessionsRetrieveDecisionResponseFaceMatchesWarnings", + "SessionsRetrieveDecisionResponseIdVerifications", + "SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails", + "SessionsRetrieveDecisionResponseIdVerificationsExtraFields", + "SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom", + "SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore", + "SessionsRetrieveDecisionResponseIdVerificationsIdLookup", + "SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison", + "SessionsRetrieveDecisionResponseIdVerificationsMatches", + "SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails", + "SessionsRetrieveDecisionResponseIdVerificationsMrz", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddress", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation", + "SessionsRetrieveDecisionResponseIdVerificationsWalletVerification", + "SessionsRetrieveDecisionResponseIdVerificationsWarnings", + "SessionsRetrieveDecisionResponseIpAnalyses", + "SessionsRetrieveDecisionResponseIpAnalysesIdDocument", + "SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation", + "SessionsRetrieveDecisionResponseIpAnalysesIp", + "SessionsRetrieveDecisionResponseIpAnalysesIpLocation", + "SessionsRetrieveDecisionResponseIpAnalysesMatches", + "SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo", + "SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo", + "SessionsRetrieveDecisionResponseIpAnalysesPoaDocument", + "SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation", + "SessionsRetrieveDecisionResponseIpAnalysesWarnings", + "SessionsRetrieveDecisionResponseKeyPeopleChecks", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures", + "SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary", + "SessionsRetrieveDecisionResponseLivenessChecks", + "SessionsRetrieveDecisionResponseLivenessChecksMatches", + "SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails", + "SessionsRetrieveDecisionResponseLivenessChecksWarnings", + "SessionsRetrieveDecisionResponseNfcVerifications", + "SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity", + "SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary", + "SessionsRetrieveDecisionResponseNfcVerificationsChipData", + "SessionsRetrieveDecisionResponseNfcVerificationsWarnings", + "SessionsRetrieveDecisionResponsePhoneVerifications", + "SessionsRetrieveDecisionResponsePhoneVerificationsCarrier", + "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle", + "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails", + "SessionsRetrieveDecisionResponsePhoneVerificationsMatches", + "SessionsRetrieveDecisionResponsePhoneVerificationsWarnings", + "SessionsRetrieveDecisionResponsePoaVerifications", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionsRetrieveDecisionResponsePoaVerificationsExtraFields", + "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress", + "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation", + "SessionsRetrieveDecisionResponsePoaVerificationsWarnings", + "SessionsRetrieveDecisionResponseQuestionnaireResponses", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSections", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices", + "SessionsRetrieveDecisionResponseRegistryChecks", + "SessionsRetrieveDecisionResponseRegistryChecksCompany", + "SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners", + "SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers", + "SessionsRetrieveDecisionResponseResubmissions", + "SessionsRetrieveDecisionResponseResubmissionsNodes", + "SessionsRetrieveDecisionResponseResubmissionsRequestedBy", + "SessionsRetrieveDecisionResponseReviews", + "SessionsRetrieveDeletionJobResponse", + "SessionsRetrieveDeletionJobResponseOutcomes", + "SessionsReviewsCreateParams", + "SessionsReviewsCreateResponse", + "SessionsReviewsListResponse", + "SessionsShareParams", + "SessionsShareResponse", + "SessionsUpdateAmlHitStatusParams", + "SessionsUpdateDataParams", + "SessionsUpdateDataParamsParsedAddress", + "SessionsUpdateDataResponse", + "SessionsUpdateDataResponseParsedAddress", + "SessionsUpdateFeatureStatusParams", + "SessionsUpdateFeatureStatusResponse", + "SessionsUpdateKybCompanyDataParams", + "SessionsUpdateKybCompanyDataResponse", + "SessionsUpdateKybPartyParams", + "SessionsUpdateKybPartyResponse", + "SessionsUpdatePoaDataParams", + "SessionsUpdatePoaDataParamsPoaParsedAddress", + "SessionsUpdatePoaDataResponse", + "SessionsUpdatePoaDataResponsePoaParsedAddress", + "SessionsUpdateStatusParams", + "SessionsUpdateStatusParamsNodesToResubmit", + "SessionsUpdateStatusResponse", + "SimpleQuestionnaireElement", + "SimpleQuestionnaireElementOptions", + "SimpleQuestionnaireElementValidation", + "SimpleWorkflowFeature", + "SocialFootprintCategorySummary", + "SocialFootprintPlatform", + "SocialFootprintProfile", + "TransactionDetail", + "TransactionDetailActionRequiredVerificationSession", + "TransactionDetailActionRequiredWalletOwnership", + "TransactionDetailActivities", + "TransactionDetailAlerts", + "TransactionDetailDocuments", + "TransactionDetailDocumentsFiles", + "TransactionDetailNetworkSnapshot", + "TransactionDetailParties", + "TransactionDetailPartiesDevice", + "TransactionDetailPartiesDeviceNetworkContext", + "TransactionDetailPaymentMethods", + "TransactionDetailProviderResults", + "TransactionDetailRemediation", + "TransactionDetailRuleRuns", + "TransactionDetailTags", + "TransactionDetailTravelRule", + "TransactionRuleAction", + "TransactionRuleActionParam", + "TransactionRuleActionVariant1", + "TransactionRuleActionVariant1Param", + "TransactionRuleActionVariant2", + "TransactionRuleActionVariant2Param", + "TransactionRuleActionVariant3", + "TransactionRuleActionVariant3Param", + "TransactionRuleActionVariant4", + "TransactionRuleActionVariant4Param", + "TransactionRuleActionVariant5", + "TransactionRuleActionVariant5Param", + "TransactionRuleActionVariant6", + "TransactionRuleActionVariant6Param", + "TransactionRuleAggregation", + "TransactionRuleAggregationParam", + "TransactionRuleBacktestResult", + "TransactionRuleCondition", + "TransactionRuleConditionParam", + "TransactionRuleDetail", + "TransactionRuleInstallResult", + "TransactionRuleLibraryEntry", + "TransactionRuleScope", + "TransactionRuleScopeParam", + "TransactionsCreateParams", + "TransactionsCreateParamsCounterparty", + "TransactionsCreateParamsCounterpartyDeviceContext", + "TransactionsCreateParamsCounterpartyDeviceContextNetworkContext", + "TransactionsCreateParamsCounterpartyPaymentMethod", + "TransactionsCreateParamsNetworkSnapshot", + "TransactionsCreateParamsSubject", + "TransactionsCreateParamsSubjectAddress", + "TransactionsCreateParamsSubjectDeviceContext", + "TransactionsCreateParamsSubjectDeviceContextNetworkContext", + "TransactionsCreateParamsSubjectInstitutionDetails", + "TransactionsCreateParamsSubjectPaymentMethod", + "TransactionsCreateParamsTransactionDetails", + "TransactionsCreateParamsTransactionDetailsCryptoParams", + "TransactionsCreateParamsTravelRuleDetails", + "TransactionsCreateSdkTokenParams", + "TransactionsCreateSdkTokenResponse", + "TransactionsListNetworksResponse", + "TransactionsListPartyNetworksResponse", + "TransactionsListResponse", + "TransactionsListResponseResults", + "TransactionsListResponseResultsTags", + "TransactionsRulesBacktestParams", + "TransactionsRulesCreateParams", + "TransactionsRulesInstallLibraryParams", + "TransactionsRulesListLibraryResponse", + "TransactionsRulesListResponse", + "TransactionsRulesUninstallLibraryParams", + "TransactionsRulesUpdateParams", + "TransactionsTravelRuleConfirmOwnershipParams", + "TransactionsTravelRuleUpdateTransferParams", + "TravelRuleCreateWidgetSessionParams", + "TravelRuleInboundResponse", + "TravelRuleListVaspsResponse", + "TravelRuleRegisterInboundParams", + "TravelRuleRegisterInboundParamsOriginatingVasp", + "TravelRuleSettingsDetail", + "TravelRuleSettingsDetailNetworks", + "TravelRuleSettingsDetailNetworksGtr", + "TravelRuleSettingsDetailNetworksSygna", + "TravelRuleSettingsDetailNetworksTrust", + "TravelRuleSettingsDetailNetworksVerifyvasp", + "TravelRuleTransferDetail", + "TravelRuleTransferDetailCounterpartyVasp", + "TravelRuleTransferDetailTimeline", + "TravelRuleUpdateSettingsParams", + "TravelRuleWalletAddressesCreateParams", + "TravelRuleWalletAddressesListResponse", + "TravelRuleWalletAddressesUpdateParams", + "TravelRuleWidgetSessionResponse", + "UserDetailItem", + "UserDetailItemComments", + "UserListItem", + "UserListItemFeaturesList", + "UserListItemTags", + "UsersBatchDeleteParams", + "UsersBatchDeleteResponse", + "UsersCreateParams", + "UsersListNetworksResponse", + "UsersListResponse", + "UsersRetrieveOngoingMonitoringJobResponse", + "UsersRetrieveOngoingMonitoringJobResponseEstimate", + "UsersRetrieveOngoingMonitoringJobResponseOutcomes", + "UsersSetAmlMonitoringParams", + "UsersSetAmlMonitoringResponse202", + "UsersSetAmlMonitoringResponse202Estimate", + "UsersSetAmlMonitoringResponse202Outcomes", + "UsersSetAmlMonitoringResponse202Results", + "UsersSetAmlMonitoringResponseVariant1", + "UsersSetAmlMonitoringResponseVariant2", + "UsersSetAmlMonitoringResponseVariant2Estimate", + "UsersSetAmlMonitoringResponseVariant2Outcomes", + "UsersSetAmlMonitoringResponseVariant2Results", + "UsersSetOngoingMonitoringParams", + "UsersSetOngoingMonitoringResponse202", + "UsersSetOngoingMonitoringResponse202Estimate", + "UsersSetOngoingMonitoringResponse202Outcomes", + "UsersSetOngoingMonitoringResponseVariant1", + "UsersSetOngoingMonitoringResponseVariant2", + "UsersSetOngoingMonitoringResponseVariant2Estimate", + "UsersSetOngoingMonitoringResponseVariant2Outcomes", + "UsersUpdateParams", + "UsersUpdateStatusParams", + "UsersUploadFaceParams", + "UsersUploadFaceResponse", + "VaspDirectoryEntry", + "VerifyEmailResponse", + "WalletAddressEntry", + "WalletAddressEntryUpdate", + "WalletScreeningResult", + "WalletScreeningResultCounterpartyConnections", + "WalletScreeningResultDestinationOfFunds", + "WalletScreeningResultRiskFactors", + "WalletScreeningResultSourceOfFunds", + "WalletScreeningScreenParams", + "WalletScreeningScreenPdfParams", + "WalletScreeningScreenPdfParamsCounterpartyConnections", + "WalletScreeningScreenPdfParamsDestinationOfFunds", + "WalletScreeningScreenPdfParamsRiskFactors", + "WalletScreeningScreenPdfParamsSourceOfFunds", + "WebhookEvent", + "WebhookSubscribedEvents", + "WebhookSubscribedEventsParam", + "WebhooksDestinationsCreateParams", + "WebhooksDestinationsCreateResponse", + "WebhooksDestinationsCreateResponseSummary", + "WebhooksDestinationsListResponseItem", + "WebhooksDestinationsRetrieveResponse", + "WebhooksDestinationsRetrieveResponseSummary", + "WebhooksDestinationsUpdateParams", + "WebhooksDestinationsUpdateResponse", + "WebhooksDestinationsUpdateResponseSummary", + "WorkflowListItem", + "WorkflowListItemCreatedBy", + "WorkflowsCreateParams", + "WorkflowsListResponse", + "WorkflowsUpdateParams", +] + + +def __getattr__(name: str) -> Any: + module = _MODULES.get(name) + if module is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + value = getattr(importlib.import_module(f"didit.types.{module}"), name) + globals()[name] = value + return value + + +def __dir__() -> list[str]: + return __all__ diff --git a/src/didit/types/account/__init__.py b/src/didit/types/account/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/account/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/account/applications/__init__.py b/src/didit/types/account/applications/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/account/applications/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/account/applications/create_params.py b/src/didit/types/account/applications/create_params.py new file mode 100644 index 0000000..c21f578 --- /dev/null +++ b/src/didit/types/account/applications/create_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["AccountApplicationsCreateParams"] + + +class AccountApplicationsCreateParams(TypedDict, total=False): + """All fields are optional. Sending an empty body creates an application with a default name.""" + + name: str + """Application display name. Defaults to `" App"`.""" + + website_url: str + """Website or app URL associated with this application.""" + + redirect_uris: Sequence[str] + """Allowed redirect URIs for OAuth-style and verification redirect flows.""" + + terms_url: str + """Terms of service URL shown in the verification flow.""" + + privacy_url: str + """Privacy policy URL shown in the verification flow.""" + + description: str + """Internal description for the application (not shown to end users).""" diff --git a/src/didit/types/account/applications/update_params.py b/src/didit/types/account/applications/update_params.py new file mode 100644 index 0000000..690fdcc --- /dev/null +++ b/src/didit/types/account/applications/update_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["AccountApplicationsUpdateParams"] + + +class AccountApplicationsUpdateParams(TypedDict, total=False): + """PATCH body for application metadata. Send only the fields you want to change; omitted fields are preserved. `uuid`, `client_id`, and `api_key` cannot be updated.""" + + name: str + """Application display name.""" + + website_url: str + """Website or app URL associated with this application.""" + + redirect_uris: Sequence[str] + """Allowed redirect URIs for OAuth-style and verification redirect flows.""" + + terms_url: str + """Terms of service URL shown in the verification flow.""" + + privacy_url: str + """Privacy policy URL shown in the verification flow.""" + + description: str + """Internal description for the application.""" diff --git a/src/didit/types/account/login_params.py b/src/didit/types/account/login_params.py new file mode 100644 index 0000000..2f2d277 --- /dev/null +++ b/src/didit/types/account/login_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AccountLoginParams"] + + +class AccountLoginParams(TypedDict, total=False): + email: Required[str] + + password: Required[str] diff --git a/src/didit/types/account/register_params.py b/src/didit/types/account/register_params.py new file mode 100644 index 0000000..c20865a --- /dev/null +++ b/src/didit/types/account/register_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AccountRegisterParams"] + + +class AccountRegisterParams(TypedDict, total=False): + email: Required[str] + """Any valid email address (personal, work, alias). Used as the unique account identifier.""" + + password: Required[str] + """Minimum 8 characters. Must contain at least one uppercase letter (`A-Z`), one lowercase letter (`a-z`), one digit (`0-9`), and one special character from `!@#$%^&*()_+-=[]{}|;:,.<>?`. Validation reports one failed rule at a time.""" diff --git a/src/didit/types/account/resend_otp_params.py b/src/didit/types/account/resend_otp_params.py new file mode 100644 index 0000000..8a95b50 --- /dev/null +++ b/src/didit/types/account/resend_otp_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AccountResendOtpParams"] + + +class AccountResendOtpParams(TypedDict, total=False): + email: Required[str] + """Email of the pending (unverified) account.""" + + password: Required[str] + """Password chosen at registration.""" diff --git a/src/didit/types/account/verify_email_params.py b/src/didit/types/account/verify_email_params.py new file mode 100644 index 0000000..d4e4b4d --- /dev/null +++ b/src/didit/types/account/verify_email_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["AccountVerifyEmailParams"] + + +class AccountVerifyEmailParams(TypedDict, total=False): + email: Required[str] + """Email used during `POST /programmatic/register/`.""" + + code: Required[str] + """6-character alphanumeric code from the verification email. Codes expire 10 minutes after the most recent code was sent.""" diff --git a/src/didit/types/age_estimation/__init__.py b/src/didit/types/age_estimation/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/age_estimation/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/age_estimation/estimate_params.py b/src/didit/types/age_estimation/estimate_params.py new file mode 100644 index 0000000..20c2c1d --- /dev/null +++ b/src/didit/types/age_estimation/estimate_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["AgeEstimationEstimateParams"] + + +class AgeEstimationEstimateParams(TypedDict, total=False): + user_image: Required[FileTypes] + """Face photo to analyze (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp` (no PDF). Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.3 MB before processing, so very high resolutions do not improve accuracy.""" + + face_liveness_score_decline_threshold: float + """Liveness threshold (0–100). A liveness `score` **at or below** this value adds a `LOW_LIVENESS_SCORE` warning and declines. Raise it for stricter anti-spoofing; values outside 0–100 return `400`.""" + + age_estimation_decline_threshold: int + """Minimum acceptable estimated age in years. An estimated age **below** this value adds an `AGE_BELOW_MINIMUM` warning and declines. Set `0` to disable the age check (the call then only runs liveness + age estimation without an age-based decision).""" + + rotate_image: bool + """When `true`, the service tries the input image in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection - useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images.""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the detected face added to your application's face index. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.""" + + vendor_data: str + """Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"age-gate"}`) - it is parsed into an object.""" diff --git a/src/didit/types/age_estimation/estimate_response.py b/src/didit/types/age_estimation/estimate_response.py new file mode 100644 index 0000000..22b0972 --- /dev/null +++ b/src/didit/types/age_estimation/estimate_response.py @@ -0,0 +1,105 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "AgeEstimationEstimateResponse", + "AgeEstimationEstimateResponseAgeEstimation", + "AgeEstimationEstimateResponseAgeEstimationUserImage", + "AgeEstimationEstimateResponseAgeEstimationUserImageEntities", + "AgeEstimationEstimateResponseAgeEstimationWarnings", +] + + +class AgeEstimationEstimateResponseAgeEstimationUserImageEntities(BaseModel): + age: float | None = None + """Model-estimated age of this face, in years.""" + + bbox: list[int] | None = None + """Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.""" + + confidence: float | None = None + """Face-detection confidence (0–1).""" + + gender: str | None = None + """Model-predicted gender of the detected face (`male` or `female`). Informational only.""" + + +class AgeEstimationEstimateResponseAgeEstimationUserImage(BaseModel): + """Face-detection results for the analyzed image.""" + + entities: list[AgeEstimationEstimateResponseAgeEstimationUserImageEntities] | None = None + """One entry per detected face. Empty when no face was found (then `age_estimation` is `null` and an `AGE_NOT_DETECTED` warning is added).""" + + best_angle: int | None = None + """Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.""" + + +class AgeEstimationEstimateResponseAgeEstimationWarnings(BaseModel): + risk: ( + Literal[ + "NO_FACE_DETECTED", + "LOW_LIVENESS_SCORE", + "LIVENESS_FACE_ATTACK", + "AGE_NOT_DETECTED", + "AGE_BELOW_MINIMUM", + ] + | str + | None + ) = None + """Machine-readable risk code. `NO_FACE_DETECTED` - the liveness model found no face; `LOW_LIVENESS_SCORE` - liveness `score` at or below `face_liveness_score_decline_threshold`; `LIVENESS_FACE_ATTACK` - a presentation attack (photo/screen/mask) was detected; `AGE_NOT_DETECTED` - no face age could be estimated; `AGE_BELOW_MINIMUM` - estimated age below `age_estimation_decline_threshold`. `NO_FACE_DETECTED` and `LOW_LIVENESS_SCORE` are mutually exclusive - when no face is found only `NO_FACE_DETECTED` is emitted.""" + + feature: Literal["LIVENESS"] | str | None = None + """Feature that raised the warning. Always `LIVENESS` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """Always `null` for age-estimation warnings.""" + + log_type: Literal["error"] | str | None = None + """Severity. Every warning this endpoint produces is an `error` and sets `status` to `Declined`.""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class AgeEstimationEstimateResponseAgeEstimation(BaseModel): + status: Literal["Approved", "Declined"] | str | None = None + """`Approved` when no warnings were raised; `Declined` otherwise (every warning this endpoint produces declines).""" + + method: Literal["PASSIVE"] | str | None = None + """Liveness method. Always `PASSIVE` on this endpoint (single-image, no user challenge).""" + + score: float | None = None + """Passive-liveness score from 0 to 100 - the model's confidence that the photo shows a live person. Compared against `face_liveness_score_decline_threshold`; at or below it the call declines with `LOW_LIVENESS_SCORE`. `null` when the liveness model returns no confidence - the call still declines with `LOW_LIVENESS_SCORE` because null is treated as 0 for the threshold comparison.""" + + user_image: AgeEstimationEstimateResponseAgeEstimationUserImage | None = None + """Face-detection results for the analyzed image.""" + + age_estimation: float | None = None + """Model-predicted age in years of the **largest** detected face. `null` when no face age could be estimated (an `AGE_NOT_DETECTED` warning is added and the call declines).""" + + warnings: list[AgeEstimationEstimateResponseAgeEstimationWarnings] | None = None + """Empty on a clean approval. Any entry sets `status` to `Declined`.""" + + +class AgeEstimationEstimateResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + age_estimation: AgeEstimationEstimateResponseAgeEstimation | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """ISO 8601 timestamp (UTC) of when the response was generated.""" diff --git a/src/didit/types/aml/__init__.py b/src/didit/types/aml/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/aml/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/aml/screen_params.py b/src/didit/types/aml/screen_params.py new file mode 100644 index 0000000..e0a05ca --- /dev/null +++ b/src/didit/types/aml/screen_params.py @@ -0,0 +1,62 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["AmlScreenParams"] + + +class AmlScreenParams(TypedDict, total=False): + full_name: Required[str] + """Full name of the person or company to screen. Minimum **2 characters** - the screening provider's own floor. A shorter or blank name returns `400` on this field before any screening runs, so nothing is billed.""" + + entity_type: Literal["person", "company"] | str + """Type of entity to screen. Defaults to `person`.""" + + date_of_birth: str | _dt.date + """Date of birth (persons) or incorporation date (companies), `YYYY-MM-DD`. Improves match precision via the DOB weight.""" + + nationality: str + """ISO 3166-1 **alpha-2** country code (e.g. `ES`). Invalid codes return `400`. Improves match precision via the country weight.""" + + document_number: str + """Identity document number. Not weighted - acts as a golden key: an exact match overrides the hit's `match_score` to 100, a hard mismatch penalizes it. Sent to the screening provider as a search filter only when it is **2-50 characters**; a value outside that range is accepted and stored, and the screening still runs and is billed, just not narrowed by document number.""" + + aml_score_approve_threshold: float + """Risk-score threshold at or below which the result is `Approved`. Must be ≤ `aml_score_review_threshold` (else `400`).""" + + aml_score_review_threshold: float + """Risk-score threshold above which the result is `Declined`. Scores between the two thresholds produce `In Review`.""" + + aml_name_weight: int + """Weight of name similarity in the per-hit match score. The three weights must sum to exactly 100 (else `400`).""" + + aml_dob_weight: int + """Weight of date of birth in the per-hit match score.""" + + aml_country_weight: int + """Weight of country/nationality in the per-hit match score.""" + + aml_match_score_threshold: int + """Per-hit cutoff: hits with `match_score` below this are `False Positive` (excluded from risk assessment); at or above are `Unreviewed` possible matches.""" + + include_adverse_media: bool + """Also search news media for negative coverage. Adds up to ~30 s of latency while results are gathered - use a ≥60 s client timeout.""" + + include_ongoing_monitoring: bool + """Keep re-screening this profile and push changes via webhook. Requires `save_api_request=true` (else `400`).""" + + save_api_request: bool + """Persist the screening as a session (console visibility, decision endpoint, webhooks, hit review). Set to `false` for a stateless check.""" + + vendor_data: str + """Your identifier for the screened user; echoed back and stored with the session.""" + + metadata: Mapping[str, object] | None + """Free-form JSON stored with the request and echoed back.""" + + sandbox_scenario: str + """Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. A standalone screening has no session to arm, so the slug goes on the screening request itself: `decline_aml_hit` returns one production-shaped hit and settles the screening as `Declined`, `review_aml_possible_match` settles it `In Review`, and a scenario that arms no AML outcome leaves the screening clean. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing).""" diff --git a/src/didit/types/aml/screen_response.py b/src/didit/types/aml/screen_response.py new file mode 100644 index 0000000..4b14229 --- /dev/null +++ b/src/didit/types/aml/screen_response.py @@ -0,0 +1,273 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "AmlScreenResponse", + "AmlScreenResponseAml", + "AmlScreenResponseAmlHits", + "AmlScreenResponseAmlHitsAdverseMediaMatches", + "AmlScreenResponseAmlHitsLinkedEntities", + "AmlScreenResponseAmlHitsPepMatches", + "AmlScreenResponseAmlHitsScoreBreakdown", + "AmlScreenResponseAmlScreenedData", + "AmlScreenResponseAmlWarnings", +] + + +class AmlScreenResponseAmlHitsAdverseMediaMatches(BaseModel): + headline: str | None = None + + summary: str | None = None + + sentiment: str | None = None + + sentiment_score: float | None = None + + source_url: str | None = None + + thumbnail: str | None = None + + country: str | None = None + + author_name: str | None = None + + publication_date: str | None = None + + adverse_keywords: list[str] | None = None + + other_sources: list[str] | None = None + + +class AmlScreenResponseAmlHitsLinkedEntities(BaseModel): + name: list[str] | None = None + + relation: list[str] | None = None + + status: list[str] | None = None + + details: list[str] | None = None + + active: list[str] | None = None + + +class AmlScreenResponseAmlHitsPepMatches(BaseModel): + list_name: str | None = None + + publisher: str | None = None + + source_url: str | None = None + + description: str | None = None + + matched_name: str | None = None + + pep_position: str | None = None + + date_of_birth: str | None = None + + place_of_birth: str | None = None + + aliases: list[str] | None = None + + education: list[str] | None = None + + other_sources: list[str] | None = None + + +class AmlScreenResponseAmlHitsScoreBreakdown(BaseModel): + """How `match_score` was computed. Name, date of birth, and country each contribute `_score` × `_weight_normalized` points; weights are re-normalized when a component is missing from the screened data. The document number is not weighted - it acts as a 'golden key': `MATCH` overrides the score to 100, `NEUTRAL` leaves it unchanged, `HARD_MISMATCH` applies a penalty.""" + + name_score: int | None = None + """Raw name similarity (0–100).""" + + name_weight: int | None = None + """Configured name weight (`aml_name_weight`).""" + + name_weight_normalized: float | None = None + """Name weight after re-normalization (0–100).""" + + name_contribution: float | None = None + """Points the name contributed to `total_score`.""" + + dob_score: int | None = None + """Date-of-birth score as a percentage of its weight (−100 to 100).""" + + dob_weight: int | None = None + """Configured DOB weight (`aml_dob_weight`).""" + + dob_weight_normalized: float | None = None + """DOB weight after re-normalization (0–100).""" + + dob_contribution: float | None = None + """Points the DOB contributed to `total_score`.""" + + country_score: int | None = None + """Country score as a percentage of its weight (−100 to 100).""" + + country_weight: int | None = None + """Configured country weight (`aml_country_weight`).""" + + country_weight_normalized: float | None = None + """Country weight after re-normalization (0–100).""" + + country_contribution: float | None = None + """Points the country contributed to `total_score`.""" + + document_number_match_type: Literal["MATCH", "NEUTRAL", "HARD_MISMATCH"] | str | None = None + """Golden-key result for the document number comparison.""" + + document_number_effect: str | None = None + """Human-readable description of the document-number effect.""" + + total_score: int | None = None + """Final weighted match score (0–100) - mirrors `match_score`.""" + + +class AmlScreenResponseAmlHits(BaseModel): + """One screening hit. `match_score` (0–100) measures identity confidence - hits below `aml_match_score_threshold` are tagged `review_status: "False Positive"` and excluded from the risk assessment; hits at or above it are `"Unreviewed"` possible matches. `risk_score` (0–100) measures how risky the matched entity is and drives the top-level `aml.score`.""" + + id: str | None = None + """Stable identifier of the matched entity in the screening datasets.""" + + url: str | None = None + """Source URL for the matched entity, when available.""" + + match: bool | None = None + """Legacy exact-match flag. Prefer `match_score` + `review_status`.""" + + score: float | None = None + """DEPRECATED weighted match score in the 0–1 range. Use `match_score` instead.""" + + match_score: float | None = None + """Identity confidence (0–100). Compared against `aml_match_score_threshold` to split False Positives from Possible Matches.""" + + risk_score: float | None = None + """Entity risk score (0–100). The highest `risk_score` among non-false-positive hits becomes the top-level `aml.score`.""" + + target: bool | None = None + + caption: str | None = None + """Display name of the matched entity.""" + + datasets: list[str] | None = None + """Datasets the entity appears in (e.g. `PEP`, `PEP Level 1`, sanctions list names).""" + + features: dict[str, object] | None = None + + properties: dict[str, object] | None = None + """Entity profile: `name`, `alias`, `notes`, `gender`, `birthDate`, `birthPlace`, `nationality`, `position`, and other biographical arrays (each nullable).""" + + rca_name: str | None = None + """Name of the related close associate through which the entity was matched, when the hit is an RCA.""" + + pep_matches: list[AmlScreenResponseAmlHitsPepMatches] | None = None + """PEP list entries backing this hit.""" + + sanction_matches: list[dict[str, object]] | None = None + """Sanctions list entries backing this hit (same backing-list shape as `pep_matches`, plus sanction-specific fields).""" + + warning_matches: list[dict[str, object]] | None = None + """Warning/watchlist entries backing this hit.""" + + adverse_media_matches: list[AmlScreenResponseAmlHitsAdverseMediaMatches] | None = None + """Adverse-media articles. Populated only when `include_adverse_media=true`.""" + + adverse_media_details: dict[str, object] | None = None + """Aggregated adverse-media sentiment for the entity (`sentiment`, `sentiment_score`, `adverse_keywords` frequency map). Populated only when `include_adverse_media=true`.""" + + first_seen: _dt.datetime | None = None + """When the entity first appeared in the datasets.""" + + last_seen: _dt.datetime | None = None + """Most recent dataset refresh that included the entity.""" + + linked_entities: list[AmlScreenResponseAmlHitsLinkedEntities] | None = None + """Known relationships of the matched entity (family, associates, companies).""" + + risk_view: dict[str, object] | None = None + """Per-dimension risk breakdown (`crimes`, `countries`, `categories`, `custom_list`), each with `score`, `weightage`, `risk_level`, and a `risk_scores` map.""" + + additional_information: dict[str, object] | None = None + """Free-form extra context, e.g. a `flag_summary` array explaining why the entity is flagged.""" + + review_status: ( + Literal["False Positive", "Unreviewed", "Confirmed Match", "Inconclusive"] | str | None + ) = None + """`False Positive` - `match_score` below `aml_match_score_threshold`, excluded from risk assessment. `Unreviewed` - possible match awaiting review. `Confirmed Match` / `Inconclusive` - set manually by a reviewer (via the Business Console) on saved sessions; that review capability is not part of this spec.""" + + score_breakdown: AmlScreenResponseAmlHitsScoreBreakdown | None = None + """How `match_score` was computed. Name, date of birth, and country each contribute `_score` × `_weight_normalized` points; weights are re-normalized when a component is missing from the screened data. The document number is not weighted - it acts as a 'golden key': `MATCH` overrides the score to 100, `NEUTRAL` leaves it unchanged, `HARD_MISMATCH` applies a penalty.""" + + +class AmlScreenResponseAmlScreenedData(BaseModel): + """Echo of what was screened: `full_name`, `date_of_birth`, `nationality`, `document_number` (absent fields are `null`).""" + + full_name: str | None = None + + date_of_birth: str | None = None + + nationality: str | None = None + + document_number: str | None = None + + +class AmlScreenResponseAmlWarnings(BaseModel): + feature: str | None = None + """Feature that raised the warning.""" + + risk: str | None = None + """Machine-readable risk identifier.""" + + additional_data: dict[str, object] | None = None + """Extra context for the warning, when available.""" + + log_type: str | None = None + """`warning`, `information`, or `error` - how the risk affected the outcome.""" + + short_description: str | None = None + """One-line human-readable summary.""" + + long_description: str | None = None + """Full human-readable explanation.""" + + +class AmlScreenResponseAml(BaseModel): + status: Literal["Approved", "In Review", "Declined"] | str | None = None + """`Declined` when `score` > `aml_score_review_threshold`; `In Review` when `score` > `aml_score_approve_threshold`; `Approved` otherwise (including when no score is available).""" + + total_hits: int | None = None + """Number of entries in `hits`.""" + + entity_type: Literal["person", "company"] | str | None = None + """Echo of the screened entity type.""" + + hits: list[AmlScreenResponseAmlHits] | None = None + + score: float | None = None + """Overall AML risk score (0–100): the highest `risk_score` among hits that are not False Positives. Drives `status` via the two thresholds.""" + + screened_data: AmlScreenResponseAmlScreenedData | None = None + """Echo of what was screened: `full_name`, `date_of_birth`, `nationality`, `document_number` (absent fields are `null`).""" + + warnings: list[AmlScreenResponseAmlWarnings] | None = None + """Contains a `POSSIBLE_MATCH_FOUND` warning whenever `score` is non-zero and ≥ `aml_score_approve_threshold` (a score of exactly 0 never warns); empty otherwise.""" + + +class AmlScreenResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + aml: AmlScreenResponseAml | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/aml_monitoring/__init__.py b/src/didit/types/aml_monitoring/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/aml_monitoring/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/aml_monitoring/retrieve_job_response.py b/src/didit/types/aml_monitoring/retrieve_job_response.py new file mode 100644 index 0000000..c081e1e --- /dev/null +++ b/src/didit/types/aml_monitoring/retrieve_job_response.py @@ -0,0 +1,99 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "AmlMonitoringRetrieveJobResponse", + "AmlMonitoringRetrieveJobResponseOutcomes", + "AmlMonitoringRetrieveJobResponseResults", +] + + +class AmlMonitoringRetrieveJobResponseOutcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class AmlMonitoringRetrieveJobResponseResults(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class AmlMonitoringRetrieveJobResponse(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str | None = None + """Which profile family the job toggles.""" + + total: int | None = None + """Profiles the job processes.""" + + processed: int | None = None + """Profiles processed so far.""" + + outcomes: AmlMonitoringRetrieveJobResponseOutcomes | None = None + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[AmlMonitoringRetrieveJobResponseResults] | None = None + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API; a job started from the Business Console reports counters only.""" + + error: str | None = None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime | None = None + + completed_at: _dt.datetime | None = None diff --git a/src/didit/types/billing/__init__.py b/src/didit/types/billing/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/billing/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/billing/retrieve_balance_response.py b/src/didit/types/billing/retrieve_balance_response.py new file mode 100644 index 0000000..d8445b3 --- /dev/null +++ b/src/didit/types/billing/retrieve_balance_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BillingRetrieveBalanceResponse"] + + +class BillingRetrieveBalanceResponse(BaseModel): + balance: str + """Current pre-paid credit balance in USD, as a fixed-point decimal string. Decreases as verifications are billed and increases when a top-up settles.""" + + auto_refill_enabled: bool + """When `true`, Didit will automatically charge the saved Stripe payment method to add credits once the balance crosses `auto_refill_threshold`. Configured from Console → Billing.""" + + auto_refill_amount: str | None = None + """USD amount that will be charged on each auto-refill. `null` when auto-refill is disabled or unset.""" + + auto_refill_threshold: str | None = None + """Trigger threshold in USD. Auto-refill fires when `balance` falls below this value. `null` when auto-refill is disabled or unset.""" diff --git a/src/didit/types/billing/top_up_params.py b/src/didit/types/billing/top_up_params.py new file mode 100644 index 0000000..1e94f69 --- /dev/null +++ b/src/didit/types/billing/top_up_params.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["BillingTopUpParams"] + + +class BillingTopUpParams(TypedDict, total=False): + amount_in_dollars: Required[float] + """Amount to charge, in USD. **Must be at least 50.** Tax is added automatically at checkout based on the customer's billing address.""" + + success_url: str + """URL Stripe redirects the customer to after a successful payment. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable. Append the literal placeholder `{CHECKOUT_SESSION_ID}` if you want Stripe to substitute the session ID.""" + + cancel_url: str + """URL Stripe redirects the customer to when they abort or close the Checkout window. Currently defaults to `https://console.didit.me`. Pass a working URL explicitly; the default host is unavailable.""" diff --git a/src/didit/types/billing/top_up_response.py b/src/didit/types/billing/top_up_response.py new file mode 100644 index 0000000..db88ee7 --- /dev/null +++ b/src/didit/types/billing/top_up_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BillingTopUpResponse"] + + +class BillingTopUpResponse(BaseModel): + checkout_session_id: str + """Stripe Checkout Session identifier (e.g. `cs_test_a1B2c3...`). Useful for server-side reconciliation.""" + + checkout_session_url: str + """Single-use Stripe-hosted payment page URL. Redirect the customer here to complete payment.""" diff --git a/src/didit/types/biometric_templates/__init__.py b/src/didit/types/biometric_templates/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/biometric_templates/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/biometric_templates/batch_delete_params.py b/src/didit/types/biometric_templates/batch_delete_params.py new file mode 100644 index 0000000..1dff779 --- /dev/null +++ b/src/didit/types/biometric_templates/batch_delete_params.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +__all__ = ["BiometricTemplatesBatchDeleteParams"] + + +class BiometricTemplatesBatchDeleteParams(TypedDict, total=False): + template_uuids: Sequence[str] + """Templates to purge. Required unless `delete_all` is `true`. Already purged ids are reported as `already_purged`.""" + + delete_all: bool + """When `true`, purges every template matching the filters below (all of the application's templates when no filter is given). `template_uuids` is ignored. Irreversible.""" + + vendor_data: str + """Only with `delete_all`: case-insensitive substring match on the owning User's `vendor_data`.""" + + vendor_user_uuid: str + """Only with `delete_all`: exact match on the owning User.""" + + status: ( + Literal["pending", "active", "expired", "held_out_of_use", "purge_pending", "purge_failed"] + | str + ) + """Only with `delete_all`.""" + + source_type: Literal["session_delete", "automatic_retention", "backfill"] | str + """Only with `delete_all`.""" + + retained_from: str | _dt.datetime + """Only with `delete_all`.""" + + retained_to: str | _dt.datetime + """Only with `delete_all`. Must not be earlier than `retained_from`.""" diff --git a/src/didit/types/biometric_templates/batch_delete_response.py b/src/didit/types/biometric_templates/batch_delete_response.py new file mode 100644 index 0000000..720ce68 --- /dev/null +++ b/src/didit/types/biometric_templates/batch_delete_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BiometricTemplatesBatchDeleteResponse", "BiometricTemplatesBatchDeleteResponseResults"] + + +class BiometricTemplatesBatchDeleteResponseResults(BaseModel): + uuid: str | None = None + + outcome: Literal["purged", "already_purged"] | str | None = None + + +class BiometricTemplatesBatchDeleteResponse(BaseModel): + results: list[BiometricTemplatesBatchDeleteResponseResults] | None = None diff --git a/src/didit/types/biometric_templates/count_response.py b/src/didit/types/biometric_templates/count_response.py new file mode 100644 index 0000000..d28d45c --- /dev/null +++ b/src/didit/types/biometric_templates/count_response.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BiometricTemplatesCountResponse"] + + +class BiometricTemplatesCountResponse(BaseModel): + count: int | None = None diff --git a/src/didit/types/biometric_templates/delete_response.py b/src/didit/types/biometric_templates/delete_response.py new file mode 100644 index 0000000..24f5c50 --- /dev/null +++ b/src/didit/types/biometric_templates/delete_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BiometricTemplatesDeleteResponse"] + + +class BiometricTemplatesDeleteResponse(BaseModel): + uuid: str | None = None + + outcome: Literal["purged", "already_purged"] | str | None = None diff --git a/src/didit/types/biometric_templates/list_response.py b/src/didit/types/biometric_templates/list_response.py new file mode 100644 index 0000000..c8f5ae4 --- /dev/null +++ b/src/didit/types/biometric_templates/list_response.py @@ -0,0 +1,97 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BiometricTemplatesListResponse", "BiometricTemplatesListResponseResults"] + + +class BiometricTemplatesListResponseResults(BaseModel): + uuid: str | None = None + """Template id. Randomly generated; not derived from the deleted session or its face.""" + + vendor_user_uuid: str | None = None + """The owning User.""" + + vendor_data: str | None = None + """The User's current `vendor_data`, resolved when you read the template (never copied).""" + + source_type: Literal["session_delete", "automatic_retention", "backfill"] | str | None = None + """How the template was created: a session deletion (API or Console), the application's automatic data-retention window, or a Didit-operated backfill you instructed.""" + + provenance_reference: str | None = None + """Opaque reference tying the template to its audit trail. It is not the deleted session's id and cannot be resolved to it.""" + + retention_policy: Literal["delete_with_session", "retain_until_user_deleted"] | str | None = ( + None + ) + """The application policy at retention time.""" + + retention_override: bool | None = None + """The `retain_face_embeddings` value sent on the deletion, or `null` when the policy applied.""" + + retained_at: _dt.datetime | None = None + + expires_at: _dt.datetime | None = None + """When the template expires and is purged automatically. Always set.""" + + retained_by: str | None = None + """Principal that gave the instruction: the application id for API keys, the Console member id for Console actions, or a `service:` identifier for automatic retention. Never an email address.""" + + retained_by_type: Literal["API_KEY", "CONSOLE_USER", "SYSTEM"] | str | None = None + + instruction_class: Literal["operational_session_delete", "privacy_erasure"] | str | None = None + + instruction_source: ( + Literal[ + "api", + "console", + "automatic_retention", + "backfill", + "privacy_erasure", + "explicit_template_purge", + "vendor_user_delete", + "expiry", + "reconciliation", + "migration", + "system", + ] + | str + | None + ) = None + + instruction_id: str | None = None + """The `instruction_id` sent on the deletion, or a Didit-generated id.""" + + held_out_of_use: bool | None = None + """`true` only while a legal hold applies. A held template is excluded from all matching.""" + + status: ( + Literal[ + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed", + "purged", + ] + | str + | None + ) = None + """Only `active` templates that are not held, not past `expires_at`, and whose User still exists take part in matching.""" + + purged_at: _dt.datetime | None = None + + +class BiometricTemplatesListResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[BiometricTemplatesListResponseResults] | None = None diff --git a/src/didit/types/biometric_templates/retrieve_response.py b/src/didit/types/biometric_templates/retrieve_response.py new file mode 100644 index 0000000..53bd762 --- /dev/null +++ b/src/didit/types/biometric_templates/retrieve_response.py @@ -0,0 +1,87 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BiometricTemplatesRetrieveResponse"] + + +class BiometricTemplatesRetrieveResponse(BaseModel): + uuid: str | None = None + """Template id. Randomly generated; not derived from the deleted session or its face.""" + + vendor_user_uuid: str | None = None + """The owning User.""" + + vendor_data: str | None = None + """The User's current `vendor_data`, resolved when you read the template (never copied).""" + + source_type: Literal["session_delete", "automatic_retention", "backfill"] | str | None = None + """How the template was created: a session deletion (API or Console), the application's automatic data-retention window, or a Didit-operated backfill you instructed.""" + + provenance_reference: str | None = None + """Opaque reference tying the template to its audit trail. It is not the deleted session's id and cannot be resolved to it.""" + + retention_policy: Literal["delete_with_session", "retain_until_user_deleted"] | str | None = ( + None + ) + """The application policy at retention time.""" + + retention_override: bool | None = None + """The `retain_face_embeddings` value sent on the deletion, or `null` when the policy applied.""" + + retained_at: _dt.datetime | None = None + + expires_at: _dt.datetime | None = None + """When the template expires and is purged automatically. Always set.""" + + retained_by: str | None = None + """Principal that gave the instruction: the application id for API keys, the Console member id for Console actions, or a `service:` identifier for automatic retention. Never an email address.""" + + retained_by_type: Literal["API_KEY", "CONSOLE_USER", "SYSTEM"] | str | None = None + + instruction_class: Literal["operational_session_delete", "privacy_erasure"] | str | None = None + + instruction_source: ( + Literal[ + "api", + "console", + "automatic_retention", + "backfill", + "privacy_erasure", + "explicit_template_purge", + "vendor_user_delete", + "expiry", + "reconciliation", + "migration", + "system", + ] + | str + | None + ) = None + + instruction_id: str | None = None + """The `instruction_id` sent on the deletion, or a Didit-generated id.""" + + held_out_of_use: bool | None = None + """`true` only while a legal hold applies. A held template is excluded from all matching.""" + + status: ( + Literal[ + "pending", + "active", + "expired", + "held_out_of_use", + "purge_pending", + "purge_failed", + "purged", + ] + | str + | None + ) = None + """Only `active` templates that are not held, not past `expires_at`, and whose User still exists take part in matching.""" + + purged_at: _dt.datetime | None = None diff --git a/src/didit/types/business_sessions/__init__.py b/src/didit/types/business_sessions/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/business_sessions/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/business_sessions/list_networks_response.py b/src/didit/types/business_sessions/list_networks_response.py new file mode 100644 index 0000000..eb740ad --- /dev/null +++ b/src/didit/types/business_sessions/list_networks_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.entity_network_membership import EntityNetworkMembership + +__all__ = ["BusinessSessionsListNetworksResponse"] + + +class BusinessSessionsListNetworksResponse(BaseModel): + results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/businesses/__init__.py b/src/didit/types/businesses/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/businesses/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/businesses/batch_delete_params.py b/src/didit/types/businesses/batch_delete_params.py new file mode 100644 index 0000000..da93bde --- /dev/null +++ b/src/didit/types/businesses/batch_delete_params.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["BusinessesBatchDeleteParams"] + + +class BusinessesBatchDeleteParams(TypedDict, total=False): + """Provide `vendor_data_list`, `didit_internal_id_list`, or `delete_all: true`. Precedence when several are sent: `delete_all` > `vendor_data_list` > `didit_internal_id_list` (only the highest-precedence selector is used).""" + + vendor_data_list: Sequence[str] + """Your own identifiers to delete, matched exactly as sent.""" + + didit_internal_id_list: Sequence[str] + """Didit's internal UUIDs (`didit_internal_id`) to delete.""" + + delete_all: bool + """If `true`, deletes every business in the application. Cannot be combined with the list selectors.""" diff --git a/src/didit/types/businesses/batch_delete_response.py b/src/didit/types/businesses/batch_delete_response.py new file mode 100644 index 0000000..4e20fad --- /dev/null +++ b/src/didit/types/businesses/batch_delete_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BusinessesBatchDeleteResponse"] + + +class BusinessesBatchDeleteResponse(BaseModel): + deleted: int | None = None + """Number of businesses deleted (excludes vendor_data values that didn't match anything).""" diff --git a/src/didit/types/businesses/create_params.py b/src/didit/types/businesses/create_params.py new file mode 100644 index 0000000..6dd69c1 --- /dev/null +++ b/src/didit/types/businesses/create_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import TypedDict + +__all__ = ["BusinessesCreateParams"] + + +class BusinessesCreateParams(TypedDict, total=False): + vendor_data: str | None + """Your unique identifier for this business (free-form string, NOT a UUID). Optional but strongly recommended. When provided, must be unique among non-deleted businesses for the application; matched exactly as sent.""" + + display_name: str | None + """Friendly display name shown in the console (takes precedence over `legal_name` for UI display).""" + + legal_name: str | None + """Official legal name of the company.""" + + registration_number: str | None + """Company registration or incorporation number.""" + + country_code: str | None + """Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`).""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str + """Initial lifecycle status. Defaults to `ACTIVE`.""" + + metadata: Mapping[str, object] | None + """Arbitrary JSON object you attach to the business. Defaults to `{}`.""" diff --git a/src/didit/types/businesses/list_networks_response.py b/src/didit/types/businesses/list_networks_response.py new file mode 100644 index 0000000..bd24e5e --- /dev/null +++ b/src/didit/types/businesses/list_networks_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.entity_network_membership import EntityNetworkMembership + +__all__ = ["BusinessesListNetworksResponse"] + + +class BusinessesListNetworksResponse(BaseModel): + results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/businesses/list_response.py b/src/didit/types/businesses/list_response.py new file mode 100644 index 0000000..ecadde6 --- /dev/null +++ b/src/didit/types/businesses/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.business_list_item import BusinessListItem + +__all__ = ["BusinessesListResponse"] + + +class BusinessesListResponse(BaseModel): + count: int | None = None + """Number of matching rows - exact up to 100, capped at 100 beyond that for performance. Do not use it to detect the last page; rely on `next` being `null` instead.""" + + next: str | None = None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[BusinessListItem] | None = None diff --git a/src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py b/src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py new file mode 100644 index 0000000..313c473 --- /dev/null +++ b/src/didit/types/businesses/retrieve_ongoing_monitoring_job_response.py @@ -0,0 +1,88 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "BusinessesRetrieveOngoingMonitoringJobResponse", + "BusinessesRetrieveOngoingMonitoringJobResponseEstimate", + "BusinessesRetrieveOngoingMonitoringJobResponseOutcomes", +] + + +class BusinessesRetrieveOngoingMonitoringJobResponseEstimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesRetrieveOngoingMonitoringJobResponseOutcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class BusinessesRetrieveOngoingMonitoringJobResponse(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: BusinessesRetrieveOngoingMonitoringJobResponseOutcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: BusinessesRetrieveOngoingMonitoringJobResponseEstimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py b/src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py new file mode 100644 index 0000000..2832d8c --- /dev/null +++ b/src/didit/types/businesses/retrieve_session_ongoing_monitoring_job_response.py @@ -0,0 +1,88 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "BusinessesRetrieveSessionOngoingMonitoringJobResponse", + "BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate", + "BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes", +] + + +class BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class BusinessesRetrieveSessionOngoingMonitoringJobResponse(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: BusinessesRetrieveSessionOngoingMonitoringJobResponseOutcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: BusinessesRetrieveSessionOngoingMonitoringJobResponseEstimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/set_aml_monitoring_params.py b/src/didit/types/businesses/set_aml_monitoring_params.py new file mode 100644 index 0000000..e3ffd2b --- /dev/null +++ b/src/didit/types/businesses/set_aml_monitoring_params.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["BusinessesSetAmlMonitoringParams"] + + +class BusinessesSetAmlMonitoringParams(TypedDict, total=False): + """Select businesses with `vendor_data_list`, `didit_internal_id_list`, or both (the two lists are OR'd). At most 1000 identifiers per request across both lists.""" + + is_enabled: Required[bool] + """true to enable monitoring, false to disable it.""" + + estimate_only: bool + """When true, returns the cost estimate and performs no writes - no screening, no billing, no job.""" + + vendor_data_list: Sequence[str] + """Your own identifiers of the businesses to toggle, matched exactly as sent.""" + + didit_internal_id_list: Sequence[str] + """Didit internal ids of the businesses to toggle - the `didit_internal_id` returned by the create, get and list endpoints.""" diff --git a/src/didit/types/businesses/set_aml_monitoring_response.py b/src/didit/types/businesses/set_aml_monitoring_response.py new file mode 100644 index 0000000..31395bb --- /dev/null +++ b/src/didit/types/businesses/set_aml_monitoring_response.py @@ -0,0 +1,299 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "BusinessesSetAmlMonitoringResponse202", + "BusinessesSetAmlMonitoringResponse202Estimate", + "BusinessesSetAmlMonitoringResponse202Outcomes", + "BusinessesSetAmlMonitoringResponse202Results", + "BusinessesSetAmlMonitoringResponseVariant1", + "BusinessesSetAmlMonitoringResponseVariant2", + "BusinessesSetAmlMonitoringResponseVariant2Estimate", + "BusinessesSetAmlMonitoringResponseVariant2Outcomes", + "BusinessesSetAmlMonitoringResponseVariant2Results", +] + + +class BusinessesSetAmlMonitoringResponseVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int + """Profiles matched by the selection.""" + + already_enabled: int + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + unmatched: list[str] + """Identifiers of the request that matched no profile in this application, exactly as sent.""" + + +class BusinessesSetAmlMonitoringResponseVariant2Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int | None = None + """Profiles matched by the selection.""" + + already_enabled: int | None = None + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int | None = None + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int | None = None + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int | None = None + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int | None = None + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float | None = None + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float | None = None + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool | None = None + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + +class BusinessesSetAmlMonitoringResponseVariant2Outcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class BusinessesSetAmlMonitoringResponseVariant2Results(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class BusinessesSetAmlMonitoringResponseVariant2(BaseModel): + job_id: str + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str + + is_enabled: bool + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str + """Which profile family the job toggles.""" + + total: int + """Profiles the job processes.""" + + processed: int + """Profiles processed so far.""" + + outcomes: BusinessesSetAmlMonitoringResponseVariant2Outcomes + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[BusinessesSetAmlMonitoringResponseVariant2Results] + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" + + error: str | None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime + + completed_at: _dt.datetime | None + + estimate: BusinessesSetAmlMonitoringResponseVariant2Estimate + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + unmatched: list[str] + """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" + + +class BusinessesSetAmlMonitoringResponse202Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int | None = None + """Profiles matched by the selection.""" + + already_enabled: int | None = None + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int | None = None + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int | None = None + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int | None = None + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int | None = None + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float | None = None + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float | None = None + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool | None = None + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + +class BusinessesSetAmlMonitoringResponse202Outcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class BusinessesSetAmlMonitoringResponse202Results(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class BusinessesSetAmlMonitoringResponse202(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str | None = None + """Which profile family the job toggles.""" + + total: int | None = None + """Profiles the job processes.""" + + processed: int | None = None + """Profiles processed so far.""" + + outcomes: BusinessesSetAmlMonitoringResponse202Outcomes | None = None + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[BusinessesSetAmlMonitoringResponse202Results] | None = None + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" + + error: str | None = None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime | None = None + + completed_at: _dt.datetime | None = None + + estimate: BusinessesSetAmlMonitoringResponse202Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + unmatched: list[str] | None = None + """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" diff --git a/src/didit/types/businesses/set_ongoing_monitoring_params.py b/src/didit/types/businesses/set_ongoing_monitoring_params.py new file mode 100644 index 0000000..9910399 --- /dev/null +++ b/src/didit/types/businesses/set_ongoing_monitoring_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["BusinessesSetOngoingMonitoringParams"] + + +class BusinessesSetOngoingMonitoringParams(TypedDict, total=False): + is_enabled: Required[bool] + """true to enable monitoring, false to disable it.""" + + estimate_only: bool + """When true, returns the cost estimate and performs no writes.""" + + update_all: bool + """Apply to every entity of the application (scoped by the filter_* params when present).""" + + filter_search: str + + filter_status: Sequence[str] + + filter_country: str + + filter_date_from: str | _dt.date + + filter_date_to: str | _dt.date + + didit_internal_id_list: Sequence[str] + """Entities to toggle. Required unless update_all is true.""" diff --git a/src/didit/types/businesses/set_ongoing_monitoring_response.py b/src/didit/types/businesses/set_ongoing_monitoring_response.py new file mode 100644 index 0000000..fdad7de --- /dev/null +++ b/src/didit/types/businesses/set_ongoing_monitoring_response.py @@ -0,0 +1,193 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "BusinessesSetOngoingMonitoringResponse202", + "BusinessesSetOngoingMonitoringResponse202Estimate", + "BusinessesSetOngoingMonitoringResponse202Outcomes", + "BusinessesSetOngoingMonitoringResponseVariant1", + "BusinessesSetOngoingMonitoringResponseVariant2", + "BusinessesSetOngoingMonitoringResponseVariant2Estimate", + "BusinessesSetOngoingMonitoringResponseVariant2Outcomes", +] + + +class BusinessesSetOngoingMonitoringResponseVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesSetOngoingMonitoringResponseVariant2Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesSetOngoingMonitoringResponseVariant2Outcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class BusinessesSetOngoingMonitoringResponseVariant2(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: BusinessesSetOngoingMonitoringResponseVariant2Outcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: BusinessesSetOngoingMonitoringResponseVariant2Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + +class BusinessesSetOngoingMonitoringResponse202Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesSetOngoingMonitoringResponse202Outcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class BusinessesSetOngoingMonitoringResponse202(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: BusinessesSetOngoingMonitoringResponse202Outcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: BusinessesSetOngoingMonitoringResponse202Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/set_session_ongoing_monitoring_params.py b/src/didit/types/businesses/set_session_ongoing_monitoring_params.py new file mode 100644 index 0000000..6da235b --- /dev/null +++ b/src/didit/types/businesses/set_session_ongoing_monitoring_params.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["BusinessesSetSessionOngoingMonitoringParams"] + + +class BusinessesSetSessionOngoingMonitoringParams(TypedDict, total=False): + is_enabled: Required[bool] + """true to enable monitoring, false to disable it.""" + + estimate_only: bool + """When true, returns the cost estimate and performs no writes.""" + + update_all: bool + """Apply to every entity of the application (scoped by the filter_* params when present).""" + + filter_search: str + + filter_status: Sequence[str] + + filter_country: str + + filter_date_from: str | _dt.date + + filter_date_to: str | _dt.date + + session_numbers: Sequence[str] + """Business session numbers to toggle. Required unless update_all (or the legacy toggle_all alias) is true.""" + + toggle_all: bool + """Legacy alias of update_all.""" + + filter_workflow_id: str diff --git a/src/didit/types/businesses/set_session_ongoing_monitoring_response.py b/src/didit/types/businesses/set_session_ongoing_monitoring_response.py new file mode 100644 index 0000000..28fc663 --- /dev/null +++ b/src/didit/types/businesses/set_session_ongoing_monitoring_response.py @@ -0,0 +1,193 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "BusinessesSetSessionOngoingMonitoringResponse202", + "BusinessesSetSessionOngoingMonitoringResponse202Estimate", + "BusinessesSetSessionOngoingMonitoringResponse202Outcomes", + "BusinessesSetSessionOngoingMonitoringResponseVariant1", + "BusinessesSetSessionOngoingMonitoringResponseVariant2", + "BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate", + "BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes", +] + + +class BusinessesSetSessionOngoingMonitoringResponseVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class BusinessesSetSessionOngoingMonitoringResponseVariant2(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: BusinessesSetSessionOngoingMonitoringResponseVariant2Outcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: BusinessesSetSessionOngoingMonitoringResponseVariant2Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + +class BusinessesSetSessionOngoingMonitoringResponse202Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class BusinessesSetSessionOngoingMonitoringResponse202Outcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class BusinessesSetSessionOngoingMonitoringResponse202(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: BusinessesSetSessionOngoingMonitoringResponse202Outcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: BusinessesSetSessionOngoingMonitoringResponse202Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/businesses/update_params.py b/src/didit/types/businesses/update_params.py new file mode 100644 index 0000000..145a780 --- /dev/null +++ b/src/didit/types/businesses/update_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import TypedDict + +__all__ = ["BusinessesUpdateParams"] + + +class BusinessesUpdateParams(TypedDict, total=False): + """Partial-update body. Provide only the fields you want to change.""" + + display_name: str | None + """Friendly display name shown in the console (takes precedence over `legal_name` for UI display).""" + + legal_name: str | None + """Official legal name from registry or manual entry.""" + + registration_number: str | None + """Company registration or incorporation number.""" + + country_code: str | None + """Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`).""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str + """Business lifecycle status. Setting `BLOCKED` automatically adds the `vendor_data` to the system blocklist; flipping away from `BLOCKED` removes that blocklist entry.""" + + metadata: Mapping[str, object] | None + """Arbitrary JSON object you attach to the business. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys.""" diff --git a/src/didit/types/businesses/update_status_params.py b/src/didit/types/businesses/update_status_params.py new file mode 100644 index 0000000..69986cd --- /dev/null +++ b/src/didit/types/businesses/update_status_params.py @@ -0,0 +1,13 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["BusinessesUpdateStatusParams"] + + +class BusinessesUpdateStatusParams(TypedDict, total=False): + status: Required[Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str] + """New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist.""" diff --git a/src/didit/types/cases/__init__.py b/src/didit/types/cases/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/apply_entity_action_params.py b/src/didit/types/cases/apply_entity_action_params.py new file mode 100644 index 0000000..032e444 --- /dev/null +++ b/src/didit/types/cases/apply_entity_action_params.py @@ -0,0 +1,29 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_action_action_enum import CaseActionActionEnum + +__all__ = ["CasesApplyEntityActionParams"] + + +class CasesApplyEntityActionParams(TypedDict, total=False): + client_reference: str + + retry: bool + + action: Required[CaseActionActionEnum] + """* `set_status` - set_status + * `add_tags` - add_tags + * `add_note` - add_note + * `add_to_blocklist` - add_to_blocklist + """ + + status: str | None + + tags: Sequence[str] + + note: str | None diff --git a/src/didit/types/cases/assign_params.py b/src/didit/types/cases/assign_params.py new file mode 100644 index 0000000..bad97c4 --- /dev/null +++ b/src/didit/types/cases/assign_params.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesAssignParams"] + + +class CasesAssignParams(TypedDict, total=False): + assignee_user_id: Required[str | None] + """null to unassign.""" + + assignee_email: str | None + + assignee_name: str | None diff --git a/src/didit/types/cases/blueprints/__init__.py b/src/didit/types/cases/blueprints/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/blueprints/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/blueprints/create_params.py b/src/didit/types/cases/blueprints/create_params.py new file mode 100644 index 0000000..f00ddd7 --- /dev/null +++ b/src/didit/types/cases/blueprints/create_params.py @@ -0,0 +1,48 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_content_config import CaseContentConfigParam + +__all__ = ["CasesBlueprintsCreateParams"] + + +class CasesBlueprintsCreateParams(TypedDict, total=False): + name: Required[str] + """Max 24 characters, e.g. "AML investigation".""" + + description: str | None + + assignee_pool: Required[Sequence[str]] + """List of User UUIDs. Empty arrays are rejected with "Pick at least one assignee".""" + + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str + + max_active_cases: int | None + """Automatic-mode threshold: "fewer than N cases of this blueprint assigned".""" + + escalation_enabled: bool + + escalation_blueprint: str | None + """Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set.""" + + transfer_enabled: bool + + four_eyes_checker_blueprint: str | None + """Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue.""" + + require_resolution_note: bool + + content_config: CaseContentConfigParam + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + is_active: bool + + default_deadline_hours: int | None + """Default deadline for newly opened cases. Existing case deadlines are unchanged.""" + + publish: bool + """Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations.""" diff --git a/src/didit/types/cases/blueprints/discard_draft_params.py b/src/didit/types/cases/blueprints/discard_draft_params.py new file mode 100644 index 0000000..18fceda --- /dev/null +++ b/src/didit/types/cases/blueprints/discard_draft_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesBlueprintsDiscardDraftParams"] + + +class CasesBlueprintsDiscardDraftParams(TypedDict, total=False): + expected_version: Required[int] + + expected_revision: Required[str | None] diff --git a/src/didit/types/cases/blueprints/install_presets_params.py b/src/didit/types/cases/blueprints/install_presets_params.py new file mode 100644 index 0000000..3e7aac8 --- /dev/null +++ b/src/didit/types/cases/blueprints/install_presets_params.py @@ -0,0 +1,13 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["CasesBlueprintsInstallPresetsParams"] + + +class CasesBlueprintsInstallPresetsParams(TypedDict, total=False): + preset_keys: Sequence[str] | None + """Subset of screening_hit, aml_investigation, suspicious_activity, kyc. Omit to install all.""" diff --git a/src/didit/types/cases/blueprints/install_presets_response.py b/src/didit/types/cases/blueprints/install_presets_response.py new file mode 100644 index 0000000..a1cfc1c --- /dev/null +++ b/src/didit/types/cases/blueprints/install_presets_response.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesBlueprintsInstallPresetsResponse"] + + +class CasesBlueprintsInstallPresetsResponse(BaseModel): + created: list[str] | None = None diff --git a/src/didit/types/cases/blueprints/list_presets_response.py b/src/didit/types/cases/blueprints/list_presets_response.py new file mode 100644 index 0000000..4d26d4a --- /dev/null +++ b/src/didit/types/cases/blueprints/list_presets_response.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.case_content_config import CaseContentConfig + +__all__ = ["CasesBlueprintsListPresetsResponse", "CasesBlueprintsListPresetsResponseResults"] + + +class CasesBlueprintsListPresetsResponseResults(BaseModel): + preset_key: ( + Literal["screening_hit", "aml_investigation", "suspicious_activity", "kyc"] | str | None + ) = None + + name: str | None = None + + description: str | None = None + + content_config: CaseContentConfig | None = None + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + installed: bool | None = None + + +class CasesBlueprintsListPresetsResponse(BaseModel): + results: list[CasesBlueprintsListPresetsResponseResults] | None = None diff --git a/src/didit/types/cases/blueprints/list_response.py b/src/didit/types/cases/blueprints/list_response.py new file mode 100644 index 0000000..317260f --- /dev/null +++ b/src/didit/types/cases/blueprints/list_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_blueprint_list_item import CaseBlueprintListItem + +__all__ = ["CasesBlueprintsListResponse"] + + +class CasesBlueprintsListResponse(BaseModel): + count: int | None = None + """Number of matching rows, capped at 100 for performance.""" + + next: str | None = None + + previous: str | None = None + + results: list[CaseBlueprintListItem] | None = None diff --git a/src/didit/types/cases/blueprints/publish_params.py b/src/didit/types/cases/blueprints/publish_params.py new file mode 100644 index 0000000..7e98d56 --- /dev/null +++ b/src/didit/types/cases/blueprints/publish_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesBlueprintsPublishParams"] + + +class CasesBlueprintsPublishParams(TypedDict, total=False): + expected_version: Required[int] + + expected_revision: Required[str | None] diff --git a/src/didit/types/cases/blueprints/save_draft_params.py b/src/didit/types/cases/blueprints/save_draft_params.py new file mode 100644 index 0000000..d1577bc --- /dev/null +++ b/src/didit/types/cases/blueprints/save_draft_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesBlueprintsSaveDraftParams"] + + +class CasesBlueprintsSaveDraftParams(TypedDict, total=False): + expected_version: Required[int] + + expected_revision: Required[str | None] + + changes: Required[Mapping[str, object]] diff --git a/src/didit/types/cases/blueprints/update_params.py b/src/didit/types/cases/blueprints/update_params.py new file mode 100644 index 0000000..80ffa99 --- /dev/null +++ b/src/didit/types/cases/blueprints/update_params.py @@ -0,0 +1,48 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit.types.components.case_content_config import CaseContentConfigParam + +__all__ = ["CasesBlueprintsUpdateParams"] + + +class CasesBlueprintsUpdateParams(TypedDict, total=False): + name: str + """Max 24 characters, e.g. "AML investigation".""" + + description: str | None + + assignee_pool: Sequence[str] + """List of User UUIDs. Empty arrays are rejected with "Pick at least one assignee".""" + + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str + + max_active_cases: int | None + """Automatic-mode threshold: "fewer than N cases of this blueprint assigned".""" + + escalation_enabled: bool + + escalation_blueprint: str | None + """Target blueprint UUID. Not enforced at save time, but the escalate action fails with 400 until both escalation_enabled and escalation_blueprint are set.""" + + transfer_enabled: bool + + four_eyes_checker_blueprint: str | None + """Setting this enables 4-eyes review: resolutions on this blueprint stage on the checker blueprint's queue.""" + + require_resolution_note: bool + + content_config: CaseContentConfigParam + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + is_active: bool + + default_deadline_hours: int | None + """Default deadline for newly opened cases. Existing case deadlines are unchanged.""" + + expected_version: int + """The version opened by the editor. A stale version returns 409 without changing the review policy.""" diff --git a/src/didit/types/cases/bulk_assign_params.py b/src/didit/types/cases/bulk_assign_params.py new file mode 100644 index 0000000..150d0e5 --- /dev/null +++ b/src/didit/types/cases/bulk_assign_params.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesBulkAssignParams"] + + +class CasesBulkAssignParams(TypedDict, total=False): + case_uuids: Required[Sequence[str]] + + assignee_user_id: Required[str | None] + + assignee_email: str | None + + assignee_name: str | None diff --git a/src/didit/types/cases/bulk_assign_response.py b/src/didit/types/cases/bulk_assign_response.py new file mode 100644 index 0000000..22646fd --- /dev/null +++ b/src/didit/types/cases/bulk_assign_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesBulkAssignResponse", "CasesBulkAssignResponseFailures"] + + +class CasesBulkAssignResponseFailures(BaseModel): + case_uuid: str | None = None + + detail: str | None = None + + +class CasesBulkAssignResponse(BaseModel): + updated: int | None = None + + failures: list[CasesBulkAssignResponseFailures] | None = None diff --git a/src/didit/types/cases/bulk_update_status_params.py b/src/didit/types/cases/bulk_update_status_params.py new file mode 100644 index 0000000..98ccdb5 --- /dev/null +++ b/src/didit/types/cases/bulk_update_status_params.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesBulkUpdateStatusParams"] + + +class CasesBulkUpdateStatusParams(TypedDict, total=False): + case_uuids: Required[Sequence[str]] + + status: Required[Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str] + + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str + """Required when status is RESOLVED.""" + + note: str | None diff --git a/src/didit/types/cases/bulk_update_status_response.py b/src/didit/types/cases/bulk_update_status_response.py new file mode 100644 index 0000000..0fc39ce --- /dev/null +++ b/src/didit/types/cases/bulk_update_status_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesBulkUpdateStatusResponse", "CasesBulkUpdateStatusResponseFailures"] + + +class CasesBulkUpdateStatusResponseFailures(BaseModel): + case_uuid: str | None = None + + detail: str | None = None + + +class CasesBulkUpdateStatusResponse(BaseModel): + updated: int | None = None + + failures: list[CasesBulkUpdateStatusResponseFailures] | None = None diff --git a/src/didit/types/cases/checklist/__init__.py b/src/didit/types/cases/checklist/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/checklist/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/checklist/toggle_item_params.py b/src/didit/types/cases/checklist/toggle_item_params.py new file mode 100644 index 0000000..b7545ca --- /dev/null +++ b/src/didit/types/cases/checklist/toggle_item_params.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesChecklistToggleItemParams"] + + +class CasesChecklistToggleItemParams(TypedDict, total=False): + is_completed: Required[bool] diff --git a/src/didit/types/cases/create_params.py b/src/didit/types/cases/create_params.py new file mode 100644 index 0000000..8ba4466 --- /dev/null +++ b/src/didit/types/cases/create_params.py @@ -0,0 +1,50 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_duplicate_case_fraud_source_context_request import ( + CaseDuplicateCaseFraudSourceContextRequest, +) + +__all__ = ["CasesCreateParams", "CasesCreateParamsSubject"] + + +class CasesCreateParamsSubject(TypedDict, total=False): + """Required unless links is provided; the case's exactly-one subject.""" + + type: Literal["user", "business"] | str + + uuid: str + + +class CasesCreateParams(TypedDict, total=False): + title: Required[str] + + blueprint: str | None + """CaseBlueprint UUID. Omit for no blueprint.""" + + priority: Literal["LOW", "MEDIUM", "HIGH"] | str + + due_at: str | _dt.datetime | None + + tags: Sequence[str] + + subject: CasesCreateParamsSubject + """Required unless links is provided; the case's exactly-one subject.""" + + links: Sequence[Mapping[str, object]] + """Sessions/business sessions/transactions to attach on creation, e.g. [{"entity_type": "transaction", "entity_uuid": "..."}]. When subject is omitted, the subject is derived from the first link's owner.""" + + assigned_to: str | None + """User UUID to assign immediately.""" + + review_duplicates: bool + """Recheck existing investigations before creating the case. When true, a subject with existing cases requires a nonblank separate_incident_reason. The console always enables this protection. Omitting it preserves existing API behavior.""" + + separate_incident_reason: str + + source_context: CaseDuplicateCaseFraudSourceContextRequest diff --git a/src/didit/types/cases/find_duplicates_params.py b/src/didit/types/cases/find_duplicates_params.py new file mode 100644 index 0000000..f06f64d --- /dev/null +++ b/src/didit/types/cases/find_duplicates_params.py @@ -0,0 +1,23 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +from didit.types.components.case_duplicate_case_link_create_request import ( + CaseDuplicateCaseLinkCreateRequest, +) +from didit.types.components.case_duplicate_case_subject_input_request import ( + CaseDuplicateCaseSubjectInputRequest, +) + +__all__ = ["CasesFindDuplicatesParams"] + + +class CasesFindDuplicatesParams(TypedDict, total=False): + subject: CaseDuplicateCaseSubjectInputRequest + + links: Sequence[CaseDuplicateCaseLinkCreateRequest] + + offset: int diff --git a/src/didit/types/cases/links/__init__.py b/src/didit/types/cases/links/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/links/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/links/create_params.py b/src/didit/types/cases/links/create_params.py new file mode 100644 index 0000000..dc1da25 --- /dev/null +++ b/src/didit/types/cases/links/create_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesLinksCreateParams"] + + +class CasesLinksCreateParams(TypedDict, total=False): + entity_type: Required[Literal["session", "business_session", "transaction"] | str] + + entity_uuid: Required[str] diff --git a/src/didit/types/cases/list_events_response.py b/src/didit/types/cases/list_events_response.py new file mode 100644 index 0000000..a8d9fb9 --- /dev/null +++ b/src/didit/types/cases/list_events_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_event import CaseEvent + +__all__ = ["CasesListEventsResponse"] + + +class CasesListEventsResponse(BaseModel): + count: int | None = None + """Number of matching rows. Exact up to 100, capped at 100 beyond that for performance. Rely on `next` being null to detect the last page.""" + + next: str | None = None + """Absolute URL of the next page, or null.""" + + previous: str | None = None + """Absolute URL of the previous page, or null.""" + + results: list[CaseEvent] | None = None diff --git a/src/didit/types/cases/list_response.py b/src/didit/types/cases/list_response.py new file mode 100644 index 0000000..d53cedc --- /dev/null +++ b/src/didit/types/cases/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_list_item import CaseListItem + +__all__ = ["CasesListResponse"] + + +class CasesListResponse(BaseModel): + count: int | None = None + """Number of matching rows. Exact up to 100, capped at 100 beyond that for performance. Rely on `next` being null to detect the last page.""" + + next: str | None = None + """Absolute URL of the next page, or null.""" + + previous: str | None = None + """Absolute URL of the previous page, or null.""" + + results: list[CaseListItem] | None = None diff --git a/src/didit/types/cases/list_reviewers_response.py b/src/didit/types/cases/list_reviewers_response.py new file mode 100644 index 0000000..acbde88 --- /dev/null +++ b/src/didit/types/cases/list_reviewers_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesListReviewersResponse", "CasesListReviewersResponseResults"] + + +class CasesListReviewersResponseResults(BaseModel): + user_id: str | None = None + + email: str | None = None + + name: str | None = None + + +class CasesListReviewersResponse(BaseModel): + results: list[CasesListReviewersResponseResults] | None = None diff --git a/src/didit/types/cases/notes/__init__.py b/src/didit/types/cases/notes/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/notes/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/notes/create_attachment_upload_params.py b/src/didit/types/cases/notes/create_attachment_upload_params.py new file mode 100644 index 0000000..8ac6de6 --- /dev/null +++ b/src/didit/types/cases/notes/create_attachment_upload_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["CasesNotesCreateAttachmentUploadParams"] + + +class CasesNotesCreateAttachmentUploadParams(TypedDict, total=False): + file: Required[FileTypes] diff --git a/src/didit/types/cases/notes/create_attachment_upload_response.py b/src/didit/types/cases/notes/create_attachment_upload_response.py new file mode 100644 index 0000000..230751f --- /dev/null +++ b/src/didit/types/cases/notes/create_attachment_upload_response.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesNotesCreateAttachmentUploadResponse"] + + +class CasesNotesCreateAttachmentUploadResponse(BaseModel): + s3_key: str | None = None + + url: str | None = None + + filename: str | None = None + + file_size: int | None = None diff --git a/src/didit/types/cases/notes/create_params.py b/src/didit/types/cases/notes/create_params.py new file mode 100644 index 0000000..ff8e736 --- /dev/null +++ b/src/didit/types/cases/notes/create_params.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesNotesCreateParams"] + + +class CasesNotesCreateParams(TypedDict, total=False): + comment: Required[str] + + reply_to: str | None + + client_reference: str | None + + mentioned_emails: Sequence[str] + + is_internal: bool + + attachments: Sequence[Mapping[str, object]] + + tags: Sequence[str] diff --git a/src/didit/types/cases/notes/retrieve_attachment_url_response.py b/src/didit/types/cases/notes/retrieve_attachment_url_response.py new file mode 100644 index 0000000..0a7b115 --- /dev/null +++ b/src/didit/types/cases/notes/retrieve_attachment_url_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesNotesRetrieveAttachmentUrlResponse"] + + +class CasesNotesRetrieveAttachmentUrlResponse(BaseModel): + s3_key: str | None = None + + url: str | None = None + + filename: str | None = None diff --git a/src/didit/types/cases/notes/update_params.py b/src/didit/types/cases/notes/update_params.py new file mode 100644 index 0000000..da08d98 --- /dev/null +++ b/src/didit/types/cases/notes/update_params.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CasesNotesUpdateParams"] + + +class CasesNotesUpdateParams(TypedDict, total=False): + is_pinned: bool diff --git a/src/didit/types/cases/print_response.py b/src/didit/types/cases/print_response.py new file mode 100644 index 0000000..b0cc050 --- /dev/null +++ b/src/didit/types/cases/print_response.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CasesPrintResponse"] + + +class CasesPrintResponse(BaseModel): + url: str | None = None diff --git a/src/didit/types/cases/regulatory_reports/__init__.py b/src/didit/types/cases/regulatory_reports/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/regulatory_reports/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/regulatory_reports/create_params.py b/src/didit/types/cases/regulatory_reports/create_params.py new file mode 100644 index 0000000..a3d7376 --- /dev/null +++ b/src/didit/types/cases/regulatory_reports/create_params.py @@ -0,0 +1,67 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import TypedDict + +__all__ = ["CasesRegulatoryReportsCreateParams", "CasesRegulatoryReportsCreateParamsNarrative"] + + +class CasesRegulatoryReportsCreateParamsNarrative(TypedDict, total=False): + who: str + + what: str + + when: str + + where: str + + why: str + + how: str + + +class CasesRegulatoryReportsCreateParams(TypedDict, total=False): + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str + """Required unless a template is given; the template's report_type is used otherwise.""" + + jurisdiction: str + """Required unless a template is given.""" + + template: str | None + """Report template UUID; its field_config seeds the draft before case-derived data is merged in.""" + + transactions: Sequence[str] | None + """Transactions to attach; defaults to the case's linked transactions.""" + + rentity_id: str | None + + rentity_branch: str | None + + submission_code: Literal["E", "M"] | str + + entity_reference: str | None + + fiu_ref_number: str | None + + currency_code_local: str | None + + submission_date: str | _dt.datetime | None + + reporting_person: Mapping[str, object] + + location: Mapping[str, object] + + reason: str | None + + action: str | None + + activity: str | None + + narrative: CasesRegulatoryReportsCreateParamsNarrative + + report_indicators: Sequence[str] + + included_fields: Sequence[str] diff --git a/src/didit/types/cases/regulatory_reports/list_response.py b/src/didit/types/cases/regulatory_reports/list_response.py new file mode 100644 index 0000000..5b34f59 --- /dev/null +++ b/src/didit/types/cases/regulatory_reports/list_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.regulatory_report_detail import RegulatoryReportDetail + +__all__ = ["CasesRegulatoryReportsListResponse"] + + +class CasesRegulatoryReportsListResponse(BaseModel): + count: int | None = None + """Number of matching rows, capped at 100 for performance.""" + + next: str | None = None + + previous: str | None = None + + results: list[RegulatoryReportDetail] | None = None diff --git a/src/didit/types/cases/reject_resolution_params.py b/src/didit/types/cases/reject_resolution_params.py new file mode 100644 index 0000000..1f68e41 --- /dev/null +++ b/src/didit/types/cases/reject_resolution_params.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["CasesRejectResolutionParams"] + + +class CasesRejectResolutionParams(TypedDict, total=False): + notes: str | None diff --git a/src/didit/types/cases/requests/__init__.py b/src/didit/types/cases/requests/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/requests/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/requests/create_params.py b/src/didit/types/cases/requests/create_params.py new file mode 100644 index 0000000..72cf025 --- /dev/null +++ b/src/didit/types/cases/requests/create_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from typing_extensions import Required, TypedDict + +from didit.types.components.customer_messaging_language_enum import CustomerMessagingLanguageEnum +from didit.types.components.customer_messaging_purpose_enum import CustomerMessagingPurposeEnum + +__all__ = ["CasesRequestsCreateParams"] + + +class CasesRequestsCreateParams(TypedDict, total=False): + contact_id: Required[str] + + purpose: Required[CustomerMessagingPurposeEnum] + """* `identity_document` - identity_document + * `proof_of_address` - proof_of_address + * `business_document` - business_document + * `confirm_details` - confirm_details + * `review_in_progress` - review_in_progress + """ + + language: CustomerMessagingLanguageEnum + + request_id: Required[str] + + expires_at: Required[str | _dt.datetime] + + preview_token: Required[str] diff --git a/src/didit/types/cases/requests/preview_params.py b/src/didit/types/cases/requests/preview_params.py new file mode 100644 index 0000000..e56375a --- /dev/null +++ b/src/didit/types/cases/requests/preview_params.py @@ -0,0 +1,23 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit.types.components.customer_messaging_language_enum import CustomerMessagingLanguageEnum +from didit.types.components.customer_messaging_purpose_enum import CustomerMessagingPurposeEnum + +__all__ = ["CasesRequestsPreviewParams"] + + +class CasesRequestsPreviewParams(TypedDict, total=False): + contact_id: Required[str] + + purpose: Required[CustomerMessagingPurposeEnum] + """* `identity_document` - identity_document + * `proof_of_address` - proof_of_address + * `business_document` - business_document + * `confirm_details` - confirm_details + * `review_in_progress` - review_in_progress + """ + + language: CustomerMessagingLanguageEnum diff --git a/src/didit/types/cases/resolve_params.py b/src/didit/types/cases/resolve_params.py new file mode 100644 index 0000000..4a1ee1a --- /dev/null +++ b/src/didit/types/cases/resolve_params.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesResolveParams"] + + +class CasesResolveParams(TypedDict, total=False): + resolution: Required[Literal["FALSE_POSITIVE", "VALID_THREAT"] | str] + + note: str | None + """Required when the blueprint sets require_resolution_note.""" diff --git a/src/didit/types/cases/retrieve_statistics_response.py b/src/didit/types/cases/retrieve_statistics_response.py new file mode 100644 index 0000000..eebda1e --- /dev/null +++ b/src/didit/types/cases/retrieve_statistics_response.py @@ -0,0 +1,187 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = [ + "CasesRetrieveStatisticsResponse", + "CasesRetrieveStatisticsResponseQueue", + "CasesRetrieveStatisticsResponseSeries", + "CasesRetrieveStatisticsResponseTables", + "CasesRetrieveStatisticsResponseTablesBlueprint", + "CasesRetrieveStatisticsResponseTablesOfficer", + "CasesRetrieveStatisticsResponseTablesSource", + "CasesRetrieveStatisticsResponseTablesStatus", + "CasesRetrieveStatisticsResponseTotals", +] + + +class CasesRetrieveStatisticsResponseQueue(BaseModel): + """Live snapshot, not scoped to the date range.""" + + open: int | None = None + + overdue: int | None = None + + +class CasesRetrieveStatisticsResponseSeries(BaseModel): + date: _dt.date | None = None + + created: int | None = None + + reviewed: int | None = None + + +class CasesRetrieveStatisticsResponseTablesBlueprint(BaseModel): + key: str | None = None + """Dimension value: officer email, "blueprint_name:uuid", source, or status. "unassigned"/"none" for missing dimensions.""" + + created: int | None = None + + assigned: int | None = None + + resolved: int | None = None + + threat_rate: float | None = None + + false_positive_rate: float | None = None + + unresolved: int | None = None + + reassignment_rate: float | None = None + + escalation_rate: float | None = None + + avg_assign_hours: float | None = None + + avg_resolve_hours: float | None = None + + avg_handling_hours: float | None = None + + +class CasesRetrieveStatisticsResponseTablesOfficer(BaseModel): + key: str | None = None + """Dimension value: officer email, "blueprint_name:uuid", source, or status. "unassigned"/"none" for missing dimensions.""" + + created: int | None = None + + assigned: int | None = None + + resolved: int | None = None + + threat_rate: float | None = None + + false_positive_rate: float | None = None + + unresolved: int | None = None + + reassignment_rate: float | None = None + + escalation_rate: float | None = None + + avg_assign_hours: float | None = None + + avg_resolve_hours: float | None = None + + avg_handling_hours: float | None = None + + +class CasesRetrieveStatisticsResponseTablesSource(BaseModel): + key: str | None = None + """Dimension value: officer email, "blueprint_name:uuid", source, or status. "unassigned"/"none" for missing dimensions.""" + + created: int | None = None + + assigned: int | None = None + + resolved: int | None = None + + threat_rate: float | None = None + + false_positive_rate: float | None = None + + unresolved: int | None = None + + reassignment_rate: float | None = None + + escalation_rate: float | None = None + + avg_assign_hours: float | None = None + + avg_resolve_hours: float | None = None + + avg_handling_hours: float | None = None + + +class CasesRetrieveStatisticsResponseTablesStatus(BaseModel): + key: str | None = None + """Dimension value: officer email, "blueprint_name:uuid", source, or status. "unassigned"/"none" for missing dimensions.""" + + created: int | None = None + + assigned: int | None = None + + resolved: int | None = None + + threat_rate: float | None = None + + false_positive_rate: float | None = None + + unresolved: int | None = None + + reassignment_rate: float | None = None + + escalation_rate: float | None = None + + avg_assign_hours: float | None = None + + avg_resolve_hours: float | None = None + + avg_handling_hours: float | None = None + + +class CasesRetrieveStatisticsResponseTables(BaseModel): + officer: list[CasesRetrieveStatisticsResponseTablesOfficer] | None = None + + blueprint: list[CasesRetrieveStatisticsResponseTablesBlueprint] | None = None + + source: list[CasesRetrieveStatisticsResponseTablesSource] | None = None + + status: list[CasesRetrieveStatisticsResponseTablesStatus] | None = None + + +class CasesRetrieveStatisticsResponseTotals(BaseModel): + created: int | None = None + + assigned: int | None = None + + resolved: int | None = None + + threat_rate: float | None = None + + false_positive_rate: float | None = None + + unresolved: int | None = None + + reassignment_rate: float | None = None + + escalation_rate: float | None = None + + avg_assign_hours: float | None = None + + avg_resolve_hours: float | None = None + + avg_handling_hours: float | None = None + + +class CasesRetrieveStatisticsResponse(BaseModel): + series: list[CasesRetrieveStatisticsResponseSeries] | None = None + + tables: CasesRetrieveStatisticsResponseTables | None = None + + totals: CasesRetrieveStatisticsResponseTotals | None = None + + queue: CasesRetrieveStatisticsResponseQueue | None = None + """Live snapshot, not scoped to the date range.""" diff --git a/src/didit/types/cases/routing_rules/__init__.py b/src/didit/types/cases/routing_rules/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/routing_rules/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/routing_rules/create_params.py b/src/didit/types/cases/routing_rules/create_params.py new file mode 100644 index 0000000..0847ece --- /dev/null +++ b/src/didit/types/cases/routing_rules/create_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_routing_condition import CaseRoutingConditionParam + +__all__ = ["CasesRoutingRulesCreateParams"] + + +class CasesRoutingRulesCreateParams(TypedDict, total=False): + name: Required[str] + + description: str + + priority: int + + conditions: Required[Sequence[CaseRoutingConditionParam]] + + required_skills: Sequence[str] + + blueprint: Required[str] + + team: str | None + + is_active: bool + + publish: bool + """Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations.""" diff --git a/src/didit/types/cases/routing_rules/discard_draft_params.py b/src/didit/types/cases/routing_rules/discard_draft_params.py new file mode 100644 index 0000000..609852d --- /dev/null +++ b/src/didit/types/cases/routing_rules/discard_draft_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesRoutingRulesDiscardDraftParams"] + + +class CasesRoutingRulesDiscardDraftParams(TypedDict, total=False): + expected_version: Required[int] + + expected_revision: Required[str | None] diff --git a/src/didit/types/cases/routing_rules/list_response.py b/src/didit/types/cases/routing_rules/list_response.py new file mode 100644 index 0000000..0a0aaf4 --- /dev/null +++ b/src/didit/types/cases/routing_rules/list_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_routing_rule import CaseRoutingRule + +__all__ = ["CasesRoutingRulesListResponse"] + + +class CasesRoutingRulesListResponse(BaseModel): + results: list[CaseRoutingRule] | None = None + + count: int | None = None + """Matching count, capped at 100. Follow next to continue beyond the cap.""" + + next: str | None = None + + previous: str | None = None diff --git a/src/didit/types/cases/routing_rules/preview_params.py b/src/didit/types/cases/routing_rules/preview_params.py new file mode 100644 index 0000000..f0bb378 --- /dev/null +++ b/src/didit/types/cases/routing_rules/preview_params.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_routing_rule_create import CaseRoutingRuleCreate + +__all__ = ["CasesRoutingRulesPreviewParams"] + + +class CasesRoutingRulesPreviewParams(TypedDict, total=False): + source: Required[ + Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] | str + ] + + priority: Literal["LOW", "MEDIUM", "HIGH"] | str + + tags: Sequence[str] + + rule_uuid: str + + draft: CaseRoutingRuleCreate diff --git a/src/didit/types/cases/routing_rules/publish_params.py b/src/didit/types/cases/routing_rules/publish_params.py new file mode 100644 index 0000000..082f4d8 --- /dev/null +++ b/src/didit/types/cases/routing_rules/publish_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesRoutingRulesPublishParams"] + + +class CasesRoutingRulesPublishParams(TypedDict, total=False): + expected_version: Required[int] + + expected_revision: Required[str | None] diff --git a/src/didit/types/cases/routing_rules/save_draft_params.py b/src/didit/types/cases/routing_rules/save_draft_params.py new file mode 100644 index 0000000..b498ed4 --- /dev/null +++ b/src/didit/types/cases/routing_rules/save_draft_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesRoutingRulesSaveDraftParams"] + + +class CasesRoutingRulesSaveDraftParams(TypedDict, total=False): + expected_version: Required[int] + + expected_revision: Required[str | None] + + changes: Required[Mapping[str, object]] diff --git a/src/didit/types/cases/routing_rules/update_params.py b/src/didit/types/cases/routing_rules/update_params.py new file mode 100644 index 0000000..779b604 --- /dev/null +++ b/src/didit/types/cases/routing_rules/update_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +from didit.types.components.case_routing_condition import CaseRoutingConditionParam + +__all__ = ["CasesRoutingRulesUpdateParams"] + + +class CasesRoutingRulesUpdateParams(TypedDict, total=False): + name: str + + description: str + + priority: int + + conditions: Sequence[CaseRoutingConditionParam] + + required_skills: Sequence[str] + + blueprint: str + + team: str | None + + is_active: bool + + expected_version: int + """Version opened by the editor. When supplied, stale writes return 409 without changing the rule.""" diff --git a/src/didit/types/cases/teams/__init__.py b/src/didit/types/cases/teams/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/cases/teams/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/cases/teams/create_params.py b/src/didit/types/cases/teams/create_params.py new file mode 100644 index 0000000..9d2d95c --- /dev/null +++ b/src/didit/types/cases/teams/create_params.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_review_team_member import CaseReviewTeamMemberParam + +__all__ = ["CasesTeamsCreateParams"] + + +class CasesTeamsCreateParams(TypedDict, total=False): + name: Required[str] + + description: str + + members: Required[Sequence[CaseReviewTeamMemberParam]] + + is_active: bool diff --git a/src/didit/types/cases/teams/list_response.py b/src/didit/types/cases/teams/list_response.py new file mode 100644 index 0000000..f51bf6f --- /dev/null +++ b/src/didit/types/cases/teams/list_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_review_team import CaseReviewTeam + +__all__ = ["CasesTeamsListResponse"] + + +class CasesTeamsListResponse(BaseModel): + results: list[CaseReviewTeam] | None = None + + count: int | None = None + """Matching count, capped at 100. Follow next to continue beyond the cap.""" + + next: str | None = None + + previous: str | None = None diff --git a/src/didit/types/cases/teams/update_params.py b/src/didit/types/cases/teams/update_params.py new file mode 100644 index 0000000..f310203 --- /dev/null +++ b/src/didit/types/cases/teams/update_params.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +from didit.types.components.case_review_team_member import CaseReviewTeamMemberParam + +__all__ = ["CasesTeamsUpdateParams"] + + +class CasesTeamsUpdateParams(TypedDict, total=False): + name: str + + description: str + + members: Sequence[CaseReviewTeamMemberParam] + + is_active: bool diff --git a/src/didit/types/cases/transfer_params.py b/src/didit/types/cases/transfer_params.py new file mode 100644 index 0000000..7e87ef6 --- /dev/null +++ b/src/didit/types/cases/transfer_params.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CasesTransferParams"] + + +class CasesTransferParams(TypedDict, total=False): + target_blueprint: Required[str] diff --git a/src/didit/types/cases/update_params.py b/src/didit/types/cases/update_params.py new file mode 100644 index 0000000..b4f98aa --- /dev/null +++ b/src/didit/types/cases/update_params.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +__all__ = ["CasesUpdateParams"] + + +class CasesUpdateParams(TypedDict, total=False): + title: str + + priority: Literal["LOW", "MEDIUM", "HIGH"] | str + + due_at: str | _dt.datetime | None + + tags: Sequence[str] diff --git a/src/didit/types/components/__init__.py b/src/didit/types/components/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/components/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/components/application.py b/src/didit/types/components/application.py new file mode 100644 index 0000000..e4983ff --- /dev/null +++ b/src/didit/types/components/application.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["Application"] + + +class Application(BaseModel): + """Full application record. `uuid`, `client_id`, and `api_key` never change after creation.""" + + uuid: str | None = None + """Application UUID. Use as `{app_id}` in subsequent calls.""" + + name: str | None = None + """Application display name shown in the Didit console.""" + + client_id: str | None = None + """Public client identifier, safe to embed in OAuth-style flows.""" + + api_key: str | None = None + """Long-lived secret (also called `client_secret`). Use as the `x-api-key` header for every call to `https://verification.didit.me/v3/...` (sessions, workflows, AML, etc.). Treat as a credential; never expose client-side.""" + + website_url: str | None = None + """Website or app URL associated with this application.""" + + redirect_uris: list[str] | None = None + """Allowed redirect URIs for OAuth-style and verification redirect flows.""" + + terms_url: str | None = None + """Terms of service URL shown in the verification flow.""" + + privacy_url: str | None = None + """Privacy policy URL shown in the verification flow.""" + + description: str | None = None + """Internal description for the application (not shown to end users).""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/application_credentials_summary.py b/src/didit/types/components/application_credentials_summary.py new file mode 100644 index 0000000..3ec85b0 --- /dev/null +++ b/src/didit/types/components/application_credentials_summary.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["ApplicationCredentialsSummary"] + + +class ApplicationCredentialsSummary(BaseModel): + """Minimal application shape returned inline by `POST /programmatic/verify-email/`. Persist the `api_key` immediately.""" + + uuid: str | None = None + + name: str | None = None + + client_id: str | None = None + + api_key: str | None = None + """Use this as `x-api-key` on every verification API call.""" diff --git a/src/didit/types/components/branding_customization.py b/src/didit/types/components/branding_customization.py new file mode 100644 index 0000000..5de11db --- /dev/null +++ b/src/didit/types/components/branding_customization.py @@ -0,0 +1,68 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["BrandingCustomization"] + + +class BrandingCustomization(BaseModel): + """White-label branding for an application: colors, fonts, radii, logo URLs and UI/UX options. Logo URLs are read-only (set them by uploading images). Custom domain and white-label email are not included.""" + + color_primary: str | None = None + + color_secondary: str | None = None + + color_background: str | None = None + + color_panel: str | None = None + + color_panel_10: str | None = None + + color_on_panel_1: str | None = None + + color_on_panel_2: str | None = None + + color_on_background: str | None = None + + color_button_1: str | None = None + + color_button_2: str | None = None + + color_button_text_1: str | None = None + + color_button_text_2: str | None = None + + color_pill_text: str | None = None + + border_radius_panel: int | None = None + + border_radius_buttons: int | None = None + + font_family: str | None = None + + font_weight: str | None = None + + font_url: str | None = None + """Read-only. Resolved Google Fonts stylesheet URL for the selected family.""" + + logo_square: str | None = None + """Read-only URL of the square logo. Upload via `image_square`.""" + + logo_rectangular: str | None = None + """Read-only URL of the rectangular logo. Upload via `image_rectangular`.""" + + favicon: str | None = None + """Read-only URL of the favicon. Upload via `image_favicon`.""" + + app_public_name: str | None = None + + privacy_policy_url: str | None = None + + skip_welcome_screen: bool | None = None + + disable_login_with_didit: bool | None = None + + hide_progress_bar: bool | None = None + + callback_seconds: int | None = None diff --git a/src/didit/types/components/business_detail_item.py b/src/didit/types/components/business_detail_item.py new file mode 100644 index 0000000..8848a43 --- /dev/null +++ b/src/didit/types/components/business_detail_item.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit.types.components.business_list_item import BusinessListItem + +__all__ = ["BusinessDetailItem"] + + +class BusinessDetailItem(BusinessListItem): + """Full business detail. Extends BusinessListItem with metadata and updated_at.""" + + metadata: dict[str, object] | None = None + """Custom metadata JSON you attached to this business. Defaults to `{}`.""" + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/business_list_item.py b/src/didit/types/components/business_list_item.py new file mode 100644 index 0000000..5235642 --- /dev/null +++ b/src/didit/types/components/business_list_item.py @@ -0,0 +1,83 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["BusinessListItem", "BusinessListItemFeaturesList", "BusinessListItemTags"] + + +class BusinessListItemFeaturesList(BaseModel): + feature: str | None = None + + status: str | None = None + + +class BusinessListItemTags(BaseModel): + uuid: str | None = None + + name: str | None = None + + color: str | None = None + + +class BusinessListItem(BaseModel): + """A verified business.""" + + didit_internal_id: str | None = None + """Didit's stable internal UUID for this business.""" + + vendor_data: str | None = None + """Your unique identifier for this business (passed when creating sessions). This can be null when no vendor identifier was supplied.""" + + display_name: str | None = None + """Custom display name set by you""" + + legal_name: str | None = None + """Official legal name from registry or manual entry""" + + registration_number: str | None = None + """Company registration or incorporation number""" + + country_code: str | None = None + """Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `US`).""" + + effective_name: str | None = None + """Best available name: display_name if set, otherwise legal_name""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + """Lifecycle status of the business record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks it for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`.""" + + session_count: int | None = None + """Total number of verification sessions for this business""" + + approved_count: int | None = None + """Number of approved sessions""" + + declined_count: int | None = None + """Number of declined sessions""" + + in_review_count: int | None = None + """Number of sessions in review""" + + features: dict[str, object] | None = None + """Aggregated per-feature status across all of this business's KYB sessions. Currently the only key is `KYB` (the registry company check status). Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`.""" + + features_list: list[BusinessListItemFeaturesList] | None = None + """Same data as `features`, as an ordered array of `{feature, status}` objects.""" + + last_session_at: _dt.datetime | None = None + """Timestamp of the most recent session""" + + first_session_at: _dt.datetime | None = None + """Timestamp of the first session""" + + last_activity_at: _dt.datetime | None = None + """Timestamp of the most recent activity (status change, session update, etc.)""" + + tags: list[BusinessListItemTags] | None = None + """Tags assigned to this business""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_action_action_enum.py b/src/didit/types/components/case_action_action_enum.py new file mode 100644 index 0000000..8eb981d --- /dev/null +++ b/src/didit/types/components/case_action_action_enum.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseActionActionEnum"] + + +CaseActionActionEnum: TypeAlias = ( + Literal["set_status", "add_tags", "add_note", "add_to_blocklist"] | str +) +"""* `set_status` - set_status +* `add_tags` - add_tags +* `add_note` - add_note +* `add_to_blocklist` - add_to_blocklist +""" diff --git a/src/didit/types/components/case_action_case_action_receipt.py b/src/didit/types/components/case_action_case_action_receipt.py new file mode 100644 index 0000000..62db24b --- /dev/null +++ b/src/didit/types/components/case_action_case_action_receipt.py @@ -0,0 +1,34 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.case_action_status_enum import CaseActionStatusEnum + +__all__ = ["CaseActionCaseActionReceipt"] + + +class CaseActionCaseActionReceipt(BaseModel): + uuid: str + + client_reference: str + + action: str + + status: CaseActionStatusEnum + """* `APPLIED` - Applied + * `FAILED` - Failed + """ + + result: object = None + + error: str | None = None + + actor_email: str | None = None + + attempts: int | None = None + + created_at: _dt.datetime + + updated_at: _dt.datetime diff --git a/src/didit/types/components/case_action_case_action_receipt_page.py b/src/didit/types/components/case_action_case_action_receipt_page.py new file mode 100644 index 0000000..18d96a3 --- /dev/null +++ b/src/didit/types/components/case_action_case_action_receipt_page.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_action_case_action_receipt import CaseActionCaseActionReceipt + +__all__ = ["CaseActionCaseActionReceiptPage"] + + +class CaseActionCaseActionReceiptPage(BaseModel): + count: int + + next_offset: int | None + + results: list[CaseActionCaseActionReceipt] diff --git a/src/didit/types/components/case_action_case_entity_action_result.py b/src/didit/types/components/case_action_case_entity_action_result.py new file mode 100644 index 0000000..5ee0894 --- /dev/null +++ b/src/didit/types/components/case_action_case_entity_action_result.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_action_case_action_receipt import CaseActionCaseActionReceipt +from didit.types.components.case_action_entity_type_enum import CaseActionEntityTypeEnum + +__all__ = ["CaseActionCaseEntityActionResult"] + + +class CaseActionCaseEntityActionResult(BaseModel): + receipt: CaseActionCaseActionReceipt | None = None + + entity_type: CaseActionEntityTypeEnum | None = None + """* `user` - user + * `business` - business + """ + + entity_uuid: str | None = None + + action: str | None = None + + previous_status: str | None = None + + new_status: str | None = None + + tags_added: list[str] | None = None + + comment_uuid: str | None = None + + blocklisted: list[str] | None = None diff --git a/src/didit/types/components/case_action_entity_type_enum.py b/src/didit/types/components/case_action_entity_type_enum.py new file mode 100644 index 0000000..f3818ca --- /dev/null +++ b/src/didit/types/components/case_action_entity_type_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseActionEntityTypeEnum"] + + +CaseActionEntityTypeEnum: TypeAlias = Literal["user", "business"] | str +"""* `user` - user +* `business` - business +""" diff --git a/src/didit/types/components/case_action_status_enum.py b/src/didit/types/components/case_action_status_enum.py new file mode 100644 index 0000000..6d5c0e4 --- /dev/null +++ b/src/didit/types/components/case_action_status_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseActionStatusEnum"] + + +CaseActionStatusEnum: TypeAlias = Literal["APPLIED", "FAILED"] | str +"""* `APPLIED` - Applied +* `FAILED` - Failed +""" diff --git a/src/didit/types/components/case_assignee.py b/src/didit/types/components/case_assignee.py new file mode 100644 index 0000000..520fd0e --- /dev/null +++ b/src/didit/types/components/case_assignee.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CaseAssignee"] + + +class CaseAssignee(BaseModel): + uuid: str | None = None + + full_name: str | None = None + + email: str | None = None diff --git a/src/didit/types/components/case_blueprint_detail.py b/src/didit/types/components/case_blueprint_detail.py new file mode 100644 index 0000000..16e3272 --- /dev/null +++ b/src/didit/types/components/case_blueprint_detail.py @@ -0,0 +1,159 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "CaseBlueprintDetail", + "CaseBlueprintDetailContentConfig", + "CaseBlueprintDetailContentConfigAmlControl", + "CaseBlueprintDetailContentConfigChecklist", + "CaseBlueprintDetailContentConfigChecklistItems", + "CaseBlueprintDetailContentConfigFinancial", + "CaseBlueprintDetailContentConfigIdentity", + "CaseBlueprintDetailContentConfigReporting", + "CaseBlueprintDetailContentConfigVerifications", + "CaseBlueprintDetailCreationSources", + "CaseBlueprintDetailReceivesEscalationFrom", +] + + +class CaseBlueprintDetailContentConfigAmlControl(BaseModel): + enabled: bool | None = None + + financial: bool | None = None + + identity: bool | None = None + + +class CaseBlueprintDetailContentConfigChecklistItems(BaseModel): + label: str | None = None + + order: int | None = None + + +class CaseBlueprintDetailContentConfigChecklist(BaseModel): + enabled: bool | None = None + + optional: bool | None = None + """When true, a case can be resolved with incomplete checklist items.""" + + items: list[CaseBlueprintDetailContentConfigChecklistItems] | None = None + + +class CaseBlueprintDetailContentConfigFinancial(BaseModel): + enabled: bool | None = None + + payment_methods: bool | None = None + + transactions: bool | None = None + + +class CaseBlueprintDetailContentConfigIdentity(BaseModel): + enabled: bool | None = None + + personal_information: bool | None = None + + applicant_tags: bool | None = None + + contact_information: bool | None = None + + risk_overview: bool | None = None + + poi_documents: bool | None = None + + poa_documents: bool | None = None + + +class CaseBlueprintDetailContentConfigReporting(BaseModel): + enabled: bool | None = None + + fiu_reports: bool | None = None + + +class CaseBlueprintDetailContentConfigVerifications(BaseModel): + enabled: bool | None = None + + summary: bool | None = None + + +class CaseBlueprintDetailContentConfig(BaseModel): + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + checklist: CaseBlueprintDetailContentConfigChecklist | None = None + + aml_control: CaseBlueprintDetailContentConfigAmlControl | None = None + + identity: CaseBlueprintDetailContentConfigIdentity | None = None + + verifications: CaseBlueprintDetailContentConfigVerifications | None = None + + reporting: CaseBlueprintDetailContentConfigReporting | None = None + + financial: CaseBlueprintDetailContentConfigFinancial | None = None + + +class CaseBlueprintDetailCreationSources(BaseModel): + kind: Literal["aml", "rule", "workflow"] | str | None = None + + id: str | None = None + + name: str | None = None + + +class CaseBlueprintDetailReceivesEscalationFrom(BaseModel): + id: str | None = None + + name: str | None = None + + +class CaseBlueprintDetail(BaseModel): + uuid: str | None = None + + name: str | None = None + + description: str | None = None + + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str | None = None + + assignee_pool: list[str] | None = None + + max_active_cases: int | None = None + + escalation_enabled: bool | None = None + + transfer_enabled: bool | None = None + + require_resolution_note: bool | None = None + + is_active: bool | None = None + + preset_key: str | None = None + + created_at: _dt.datetime | None = None + + escalation_blueprint: str | None = None + + escalation_blueprint_name: str | None = None + + four_eyes_checker_blueprint: str | None = None + + four_eyes_checker_blueprint_name: str | None = None + + content_config: CaseBlueprintDetailContentConfig | None = None + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + creation_sources: list[CaseBlueprintDetailCreationSources] | None = None + """Connectors currently routing new cases into this blueprint.""" + + receives_escalation_from: list[CaseBlueprintDetailReceivesEscalationFrom] | None = None + """Other blueprints whose Escalation target points at this one.""" + + updated_at: _dt.datetime | None = None + + version: int | None = None + + default_deadline_hours: int | None = None diff --git a/src/didit/types/components/case_blueprint_list_item.py b/src/didit/types/components/case_blueprint_list_item.py new file mode 100644 index 0000000..2483a71 --- /dev/null +++ b/src/didit/types/components/case_blueprint_list_item.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["CaseBlueprintListItem"] + + +class CaseBlueprintListItem(BaseModel): + uuid: str | None = None + + name: str | None = None + + description: str | None = None + + assignment_method: Literal["MANUAL", "AUTOMATIC"] | str | None = None + + assignee_pool: list[str] | None = None + + max_active_cases: int | None = None + + escalation_enabled: bool | None = None + + transfer_enabled: bool | None = None + + require_resolution_note: bool | None = None + + is_active: bool | None = None + + preset_key: str | None = None + + created_at: _dt.datetime | None = None + + version: int | None = None + + default_deadline_hours: int | None = None diff --git a/src/didit/types/components/case_blueprint_summary.py b/src/didit/types/components/case_blueprint_summary.py new file mode 100644 index 0000000..dd40a67 --- /dev/null +++ b/src/didit/types/components/case_blueprint_summary.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CaseBlueprintSummary"] + + +class CaseBlueprintSummary(BaseModel): + uuid: str | None = None + + name: str | None = None diff --git a/src/didit/types/components/case_checklist_item.py b/src/didit/types/components/case_checklist_item.py new file mode 100644 index 0000000..8fafffe --- /dev/null +++ b/src/didit/types/components/case_checklist_item.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CaseChecklistItem"] + + +class CaseChecklistItem(BaseModel): + uuid: str | None = None + + label: str | None = None + + is_completed: bool | None = None + + completed_by: str | None = None + + completed_at: _dt.datetime | None = None + + order: int | None = None diff --git a/src/didit/types/components/case_collaboration_case_note.py b/src/didit/types/components/case_collaboration_case_note.py new file mode 100644 index 0000000..f240e66 --- /dev/null +++ b/src/didit/types/components/case_collaboration_case_note.py @@ -0,0 +1,35 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.case_collaboration_case_note_reply import CaseCollaborationCaseNoteReply + +__all__ = ["CaseCollaborationCaseNote"] + + +class CaseCollaborationCaseNote(BaseModel): + uuid: str + + comment: str + + actor_email: str | None = None + + actor_name: str | None = None + + mentioned_emails: object = None + + is_internal: bool | None = None + + attachments: object = None + + tags: object = None + + created_at: _dt.datetime + + reply_to: CaseCollaborationCaseNoteReply | None + + is_pinned: bool | None = None + + client_reference: str | None = None diff --git a/src/didit/types/components/case_collaboration_case_note_reply.py b/src/didit/types/components/case_collaboration_case_note_reply.py new file mode 100644 index 0000000..e5a6416 --- /dev/null +++ b/src/didit/types/components/case_collaboration_case_note_reply.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CaseCollaborationCaseNoteReply"] + + +class CaseCollaborationCaseNoteReply(BaseModel): + uuid: str + + actor_name: str | None + + actor_email: str | None + + comment: str diff --git a/src/didit/types/components/case_collaboration_paginated_case_note_list.py b/src/didit/types/components/case_collaboration_paginated_case_note_list.py new file mode 100644 index 0000000..dcf9dee --- /dev/null +++ b/src/didit/types/components/case_collaboration_paginated_case_note_list.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_collaboration_case_note import CaseCollaborationCaseNote + +__all__ = ["CaseCollaborationPaginatedCaseNoteList"] + + +class CaseCollaborationPaginatedCaseNoteList(BaseModel): + count: int + + next: str | None = None + + previous: str | None = None + + results: list[CaseCollaborationCaseNote] diff --git a/src/didit/types/components/case_content_config.py b/src/didit/types/components/case_content_config.py new file mode 100644 index 0000000..3a8641f --- /dev/null +++ b/src/didit/types/components/case_content_config.py @@ -0,0 +1,177 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +from didit._models import BaseModel + +__all__ = [ + "CaseContentConfig", + "CaseContentConfigAmlControl", + "CaseContentConfigAmlControlParam", + "CaseContentConfigChecklist", + "CaseContentConfigChecklistItems", + "CaseContentConfigChecklistItemsParam", + "CaseContentConfigChecklistParam", + "CaseContentConfigFinancial", + "CaseContentConfigFinancialParam", + "CaseContentConfigIdentity", + "CaseContentConfigIdentityParam", + "CaseContentConfigParam", + "CaseContentConfigReporting", + "CaseContentConfigReportingParam", + "CaseContentConfigVerifications", + "CaseContentConfigVerificationsParam", +] + + +class CaseContentConfigAmlControlParam(TypedDict, total=False): + enabled: bool + + financial: bool + + identity: bool + + +class CaseContentConfigChecklistItemsParam(TypedDict, total=False): + label: str + + order: int + + +class CaseContentConfigChecklistParam(TypedDict, total=False): + enabled: bool + + optional: bool + """When true, a case can be resolved with incomplete checklist items.""" + + items: Sequence[CaseContentConfigChecklistItemsParam] + + +class CaseContentConfigFinancialParam(TypedDict, total=False): + enabled: bool + + payment_methods: bool + + transactions: bool + + +class CaseContentConfigIdentityParam(TypedDict, total=False): + enabled: bool + + personal_information: bool + + applicant_tags: bool + + contact_information: bool + + risk_overview: bool + + poi_documents: bool + + poa_documents: bool + + +class CaseContentConfigReportingParam(TypedDict, total=False): + enabled: bool + + fiu_reports: bool + + +class CaseContentConfigVerificationsParam(TypedDict, total=False): + enabled: bool + + summary: bool + + +class CaseContentConfigParam(TypedDict, total=False): + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + checklist: CaseContentConfigChecklistParam + + aml_control: CaseContentConfigAmlControlParam + + identity: CaseContentConfigIdentityParam + + verifications: CaseContentConfigVerificationsParam + + reporting: CaseContentConfigReportingParam + + financial: CaseContentConfigFinancialParam + + +class CaseContentConfigAmlControl(BaseModel): + enabled: bool | None = None + + financial: bool | None = None + + identity: bool | None = None + + +class CaseContentConfigChecklistItems(BaseModel): + label: str | None = None + + order: int | None = None + + +class CaseContentConfigChecklist(BaseModel): + enabled: bool | None = None + + optional: bool | None = None + """When true, a case can be resolved with incomplete checklist items.""" + + items: list[CaseContentConfigChecklistItems] | None = None + + +class CaseContentConfigFinancial(BaseModel): + enabled: bool | None = None + + payment_methods: bool | None = None + + transactions: bool | None = None + + +class CaseContentConfigIdentity(BaseModel): + enabled: bool | None = None + + personal_information: bool | None = None + + applicant_tags: bool | None = None + + contact_information: bool | None = None + + risk_overview: bool | None = None + + poi_documents: bool | None = None + + poa_documents: bool | None = None + + +class CaseContentConfigReporting(BaseModel): + enabled: bool | None = None + + fiu_reports: bool | None = None + + +class CaseContentConfigVerifications(BaseModel): + enabled: bool | None = None + + summary: bool | None = None + + +class CaseContentConfig(BaseModel): + """The six togglable sections that drive which tabs and fields appear on the case page. Unknown sections or keys are rejected.""" + + checklist: CaseContentConfigChecklist | None = None + + aml_control: CaseContentConfigAmlControl | None = None + + identity: CaseContentConfigIdentity | None = None + + verifications: CaseContentConfigVerifications | None = None + + reporting: CaseContentConfigReporting | None = None + + financial: CaseContentConfigFinancial | None = None diff --git a/src/didit/types/components/case_detail.py b/src/didit/types/components/case_detail.py new file mode 100644 index 0000000..3d5a248 --- /dev/null +++ b/src/didit/types/components/case_detail.py @@ -0,0 +1,92 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.case_assignee import CaseAssignee +from didit.types.components.case_blueprint_detail import CaseBlueprintDetail +from didit.types.components.case_checklist_item import CaseChecklistItem +from didit.types.components.case_event import CaseEvent +from didit.types.components.case_link import CaseLink +from didit.types.components.case_note import CaseNote +from didit.types.components.case_subject import CaseSubject + +__all__ = ["CaseDetail", "CaseDetailPendingResolution"] + + +class CaseDetailPendingResolution(BaseModel): + """Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.""" + + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | None = None + + resolution_notes: str | None = None + + maker_email: str | None = None + + maker_name: str | None = None + + maker_blueprint_id: str | None = None + + staged_at: _dt.datetime | None = None + + +class CaseDetail(BaseModel): + uuid: str | None = None + + case_number: str | None = None + + title: str | None = None + + status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | None = None + + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | None = None + + resolution_notes: str | None = None + + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | None = None + + source: ( + Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] | str | None + ) = None + + tags: list[str] | None = None + + subject: CaseSubject | None = None + """The single user or business the case is anchored to.""" + + assigned_to: CaseAssignee | None = None + + assigned_at: _dt.datetime | None = None + + due_at: _dt.datetime | None = None + + resolved_at: _dt.datetime | None = None + + resolved_by_email: str | None = None + + pending_resolution: CaseDetailPendingResolution | None = None + """Set only while a 4-eyes resolution is staged on the checker blueprint, awaiting approve or reject-approval.""" + + metadata: dict[str, object] | None = None + + blueprint: CaseBlueprintDetail | None = None + + notes: list[CaseNote] | None = None + """Newest 50 notes.""" + + events: list[CaseEvent] | None = None + """Newest 100 events.""" + + links: list[CaseLink] | None = None + + links_count: int | None = None + + notes_count: int | None = None + + checklist_items: list[CaseChecklistItem] | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_duplicate_case_duplicate.py b/src/didit/types/components/case_duplicate_case_duplicate.py new file mode 100644 index 0000000..96508f5 --- /dev/null +++ b/src/didit/types/components/case_duplicate_case_duplicate.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.case_duplicate_priority_enum import CaseDuplicatePriorityEnum +from didit.types.components.case_duplicate_status_enum import CaseDuplicateStatusEnum + +__all__ = ["CaseDuplicateCaseDuplicate"] + + +class CaseDuplicateCaseDuplicate(BaseModel): + uuid: str + + case_number: str | None = None + + title: str + + status: CaseDuplicateStatusEnum | None = None + """* `OPEN` - Open + * `AWAITING_USER` - Awaiting user + * `RESOLVED` - Resolved + """ + + priority: CaseDuplicatePriorityEnum | None = None + """* `LOW` - Low + * `MEDIUM` - Medium + * `HIGH` - High + """ + + created_at: _dt.datetime diff --git a/src/didit/types/components/case_duplicate_case_duplicate_page.py b/src/didit/types/components/case_duplicate_case_duplicate_page.py new file mode 100644 index 0000000..9c84b10 --- /dev/null +++ b/src/didit/types/components/case_duplicate_case_duplicate_page.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_duplicate_case_duplicate import CaseDuplicateCaseDuplicate + +__all__ = ["CaseDuplicateCaseDuplicatePage"] + + +class CaseDuplicateCaseDuplicatePage(BaseModel): + count: int + + next_offset: int | None + + results: list[CaseDuplicateCaseDuplicate] diff --git a/src/didit/types/components/case_duplicate_case_fraud_source_context_request.py b/src/didit/types/components/case_duplicate_case_fraud_source_context_request.py new file mode 100644 index 0000000..528c6df --- /dev/null +++ b/src/didit/types/components/case_duplicate_case_fraud_source_context_request.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["CaseDuplicateCaseFraudSourceContextRequest"] + + +class CaseDuplicateCaseFraudSourceContextRequest(TypedDict, total=False): + check_id: Required[str] + + request_id: Required[str] + + source_section: Required[str] diff --git a/src/didit/types/components/case_duplicate_case_link_create_request.py b/src/didit/types/components/case_duplicate_case_link_create_request.py new file mode 100644 index 0000000..e68de1d --- /dev/null +++ b/src/didit/types/components/case_duplicate_case_link_create_request.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_duplicate_entity_type_enum import CaseDuplicateEntityTypeEnum + +__all__ = ["CaseDuplicateCaseLinkCreateRequest"] + + +class CaseDuplicateCaseLinkCreateRequest(TypedDict, total=False): + entity_type: Required[CaseDuplicateEntityTypeEnum] + """* `session` - Session + * `business_session` - Business Session + * `transaction` - Transaction + """ + + entity_uuid: Required[str] diff --git a/src/didit/types/components/case_duplicate_case_subject_input_request.py b/src/didit/types/components/case_duplicate_case_subject_input_request.py new file mode 100644 index 0000000..e0aeef9 --- /dev/null +++ b/src/didit/types/components/case_duplicate_case_subject_input_request.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_duplicate_type_enum import CaseDuplicateTypeEnum + +__all__ = ["CaseDuplicateCaseSubjectInputRequest"] + + +class CaseDuplicateCaseSubjectInputRequest(TypedDict, total=False): + type: Required[CaseDuplicateTypeEnum] + """* `user` - user + * `business` - business + """ + + uuid: Required[str] diff --git a/src/didit/types/components/case_duplicate_entity_type_enum.py b/src/didit/types/components/case_duplicate_entity_type_enum.py new file mode 100644 index 0000000..9e677df --- /dev/null +++ b/src/didit/types/components/case_duplicate_entity_type_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseDuplicateEntityTypeEnum"] + + +CaseDuplicateEntityTypeEnum: TypeAlias = Literal["session", "business_session", "transaction"] | str +"""* `session` - Session +* `business_session` - Business Session +* `transaction` - Transaction +""" diff --git a/src/didit/types/components/case_duplicate_priority_enum.py b/src/didit/types/components/case_duplicate_priority_enum.py new file mode 100644 index 0000000..ab47514 --- /dev/null +++ b/src/didit/types/components/case_duplicate_priority_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseDuplicatePriorityEnum"] + + +CaseDuplicatePriorityEnum: TypeAlias = Literal["LOW", "MEDIUM", "HIGH"] | str +"""* `LOW` - Low +* `MEDIUM` - Medium +* `HIGH` - High +""" diff --git a/src/didit/types/components/case_duplicate_status_enum.py b/src/didit/types/components/case_duplicate_status_enum.py new file mode 100644 index 0000000..bdcbaf1 --- /dev/null +++ b/src/didit/types/components/case_duplicate_status_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseDuplicateStatusEnum"] + + +CaseDuplicateStatusEnum: TypeAlias = Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str +"""* `OPEN` - Open +* `AWAITING_USER` - Awaiting user +* `RESOLVED` - Resolved +""" diff --git a/src/didit/types/components/case_duplicate_type_enum.py b/src/didit/types/components/case_duplicate_type_enum.py new file mode 100644 index 0000000..1596232 --- /dev/null +++ b/src/didit/types/components/case_duplicate_type_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CaseDuplicateTypeEnum"] + + +CaseDuplicateTypeEnum: TypeAlias = Literal["user", "business"] | str +"""* `user` - user +* `business` - business +""" diff --git a/src/didit/types/components/case_event.py b/src/didit/types/components/case_event.py new file mode 100644 index 0000000..306b091 --- /dev/null +++ b/src/didit/types/components/case_event.py @@ -0,0 +1,55 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["CaseEvent"] + + +class CaseEvent(BaseModel): + uuid: str | None = None + + event_type: ( + Literal[ + "CREATED", + "STATUS_CHANGED", + "ASSIGNED", + "UNASSIGNED", + "RESOLVED", + "REOPENED", + "LINK_ADDED", + "LINK_REMOVED", + "NOTE_ADDED", + "PRIORITY_CHANGED", + "DUE_DATE_CHANGED", + "TAG_ADDED", + "TAG_REMOVED", + "ESCALATED", + "TRANSFERRED", + "CHECKLIST_COMPLETED", + "REPORT_CREATED", + "REPORT_FINALIZED", + "SUBMITTED_FOR_APPROVAL", + "APPROVAL_GRANTED", + "APPROVAL_REJECTED", + "ENTITY_ACTION", + "APPROVAL_INVALIDATED", + ] + | str + | None + ) = None + + actor_email: str | None = None + + actor_name: str | None = None + + previous_status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | None = None + + new_status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | None = None + + metadata: dict[str, object] | None = None + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_link.py b/src/didit/types/components/case_link.py new file mode 100644 index 0000000..ffdffcf --- /dev/null +++ b/src/didit/types/components/case_link.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["CaseLink"] + + +class CaseLink(BaseModel): + uuid: str | None = None + + linked_entity_type: Literal["session", "business_session", "transaction"] | str | None = None + + linked_entity_uuid: str | None = None + + linked_entity_summary: dict[str, object] | None = None + """session_id/session_number/status for session and business_session links; uuid/txn_id/status for transaction links.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_list_item.py b/src/didit/types/components/case_list_item.py new file mode 100644 index 0000000..cace29d --- /dev/null +++ b/src/didit/types/components/case_list_item.py @@ -0,0 +1,66 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.case_assignee import CaseAssignee +from didit.types.components.case_blueprint_summary import CaseBlueprintSummary +from didit.types.components.case_subject import CaseSubject + +__all__ = ["CaseListItem"] + + +class CaseListItem(BaseModel): + uuid: str | None = None + + case_number: str | None = None + """Auto-assigned, e.g. CS-0001.""" + + title: str | None = None + + status: Literal["OPEN", "AWAITING_USER", "RESOLVED"] | str | None = None + + resolution: Literal["FALSE_POSITIVE", "VALID_THREAT"] | str | None = None + + priority: Literal["LOW", "MEDIUM", "HIGH"] | str | None = None + + source: ( + Literal["MANUAL", "AML_SCREENING", "TRANSACTION_RULE", "WORKFLOW_BUILDER"] | str | None + ) = None + + tags: list[str] | None = None + + subject: CaseSubject | None = None + """The single user or business the case is anchored to.""" + + assigned_to: CaseAssignee | None = None + + assigned_at: _dt.datetime | None = None + + due_at: _dt.datetime | None = None + + resolved_at: _dt.datetime | None = None + + created_by_email: str | None = None + + created_by_name: str | None = None + + blueprint: CaseBlueprintSummary | None = None + + links_count: int | None = None + + notes_count: int | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + application_id: str | None = None + + application_name: str | None = None + + organization_id: str | None = None + + organization_name: str | None = None diff --git a/src/didit/types/components/case_note.py b/src/didit/types/components/case_note.py new file mode 100644 index 0000000..8c3ef22 --- /dev/null +++ b/src/didit/types/components/case_note.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CaseNote", "CaseNoteAttachments"] + + +class CaseNoteAttachments(BaseModel): + s3_key: str | None = None + + filename: str | None = None + + file_size: int | None = None + + url: str | None = None + + +class CaseNote(BaseModel): + uuid: str | None = None + + comment: str | None = None + + actor_email: str | None = None + + actor_name: str | None = None + + mentioned_emails: list[str] | None = None + + is_internal: bool | None = None + + attachments: list[CaseNoteAttachments] | None = None + + tags: list[str] | None = None + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_publication_assignment_method_enum.py b/src/didit/types/components/case_publication_assignment_method_enum.py new file mode 100644 index 0000000..846f83f --- /dev/null +++ b/src/didit/types/components/case_publication_assignment_method_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CasePublicationAssignmentMethodEnum"] + + +CasePublicationAssignmentMethodEnum: TypeAlias = Literal["MANUAL", "AUTOMATIC"] | str +"""* `MANUAL` - Manual +* `AUTOMATIC` - Automatic +""" diff --git a/src/didit/types/components/case_publication_case_blueprint.py b/src/didit/types/components/case_publication_case_blueprint.py new file mode 100644 index 0000000..206802c --- /dev/null +++ b/src/didit/types/components/case_publication_case_blueprint.py @@ -0,0 +1,60 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.case_publication_assignment_method_enum import ( + CasePublicationAssignmentMethodEnum, +) + +__all__ = ["CasePublicationCaseBlueprint"] + + +class CasePublicationCaseBlueprint(BaseModel): + uuid: str + + name: str + + description: str | None = None + + assignment_method: CasePublicationAssignmentMethodEnum | None = None + """* `MANUAL` - Manual + * `AUTOMATIC` - Automatic + """ + + assignee_pool: object = None + + max_active_cases: int | None = None + + escalation_enabled: bool | None = None + + escalation_blueprint: str | None = None + + escalation_blueprint_name: str + + transfer_enabled: bool | None = None + + four_eyes_checker_blueprint: str | None = None + + four_eyes_checker_blueprint_name: str + + require_resolution_note: bool | None = None + + content_config: object = None + + version: int | None = None + + default_deadline_hours: int | None = None + + is_active: bool | None = None + + preset_key: str | None = None + + creation_sources: list[dict[str, object]] + + receives_escalation_from: list[dict[str, object]] + + created_at: _dt.datetime + + updated_at: _dt.datetime diff --git a/src/didit/types/components/case_publication_paginated_policy_revision_list.py b/src/didit/types/components/case_publication_paginated_policy_revision_list.py new file mode 100644 index 0000000..6abef09 --- /dev/null +++ b/src/didit/types/components/case_publication_paginated_policy_revision_list.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_publication_policy_revision import CasePublicationPolicyRevision + +__all__ = ["CasePublicationPaginatedPolicyRevisionList"] + + +class CasePublicationPaginatedPolicyRevisionList(BaseModel): + count: int + + next: str | None = None + + previous: str | None = None + + results: list[CasePublicationPolicyRevision] diff --git a/src/didit/types/components/case_publication_policy_draft.py b/src/didit/types/components/case_publication_policy_draft.py new file mode 100644 index 0000000..2506fef --- /dev/null +++ b/src/didit/types/components/case_publication_policy_draft.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CasePublicationPolicyDraft"] + + +class CasePublicationPolicyDraft(BaseModel): + revision: str | None = None + + base_version: int + + changes: object = None + + actor_email: str | None = None + + updated_at: _dt.datetime diff --git a/src/didit/types/components/case_publication_policy_draft_response.py b/src/didit/types/components/case_publication_policy_draft_response.py new file mode 100644 index 0000000..0a602f2 --- /dev/null +++ b/src/didit/types/components/case_publication_policy_draft_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.case_publication_policy_draft import CasePublicationPolicyDraft + +__all__ = ["CasePublicationPolicyDraftResponse"] + + +class CasePublicationPolicyDraftResponse(BaseModel): + draft: CasePublicationPolicyDraft | None diff --git a/src/didit/types/components/case_publication_policy_revision.py b/src/didit/types/components/case_publication_policy_revision.py new file mode 100644 index 0000000..3662546 --- /dev/null +++ b/src/didit/types/components/case_publication_policy_revision.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CasePublicationPolicyRevision"] + + +class CasePublicationPolicyRevision(BaseModel): + version: int + + snapshot: object + + actor_email: str | None = None + + published_at: _dt.datetime diff --git a/src/didit/types/components/case_review_team.py b/src/didit/types/components/case_review_team.py new file mode 100644 index 0000000..042f6e1 --- /dev/null +++ b/src/didit/types/components/case_review_team.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.case_review_team_member import CaseReviewTeamMember + +__all__ = ["CaseReviewTeam"] + + +class CaseReviewTeam(BaseModel): + name: str | None = None + + description: str | None = None + + members: list[CaseReviewTeamMember] | None = None + + is_active: bool | None = None + + uuid: str | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/case_review_team_member.py b/src/didit/types/components/case_review_team_member.py new file mode 100644 index 0000000..7ff4703 --- /dev/null +++ b/src/didit/types/components/case_review_team_member.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["CaseReviewTeamMember", "CaseReviewTeamMemberParam"] + + +class CaseReviewTeamMember(BaseModel): + user_id: str + + availability: Literal["available", "unavailable"] | str | None = None + + capacity: int | None = None + + skills: list[str] | None = None + + available_from: _dt.datetime | None = None + + available_until: _dt.datetime | None = None + + +class CaseReviewTeamMemberParam(TypedDict, total=False): + user_id: Required[str] + + availability: Literal["available", "unavailable"] | str + + capacity: int + + skills: Sequence[str] + + available_from: str | _dt.datetime | None + + available_until: str | _dt.datetime | None diff --git a/src/didit/types/components/case_routing_condition.py b/src/didit/types/components/case_routing_condition.py new file mode 100644 index 0000000..bd0ea22 --- /dev/null +++ b/src/didit/types/components/case_routing_condition.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["CaseRoutingCondition", "CaseRoutingConditionParam"] + + +class CaseRoutingCondition(BaseModel): + """Source and priority support equals or in with supported values. Tags support contains with one non-empty tag.""" + + field: Literal["source", "priority", "tags"] | str + + operator: Literal["equals", "in", "contains"] | str + + value: str | list[str] + + +class CaseRoutingConditionParam(TypedDict, total=False): + """Source and priority support equals or in with supported values. Tags support contains with one non-empty tag.""" + + field: Required[Literal["source", "priority", "tags"] | str] + + operator: Required[Literal["equals", "in", "contains"] | str] + + value: Required[str | Sequence[str]] diff --git a/src/didit/types/components/case_routing_preview.py b/src/didit/types/components/case_routing_preview.py new file mode 100644 index 0000000..5627fdc --- /dev/null +++ b/src/didit/types/components/case_routing_preview.py @@ -0,0 +1,44 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["CaseRoutingPreview", "CaseRoutingPreviewCandidates", "CaseRoutingPreviewMatchedRules"] + + +class CaseRoutingPreviewCandidates(BaseModel): + user_id: str | None = None + + eligible: bool | None = None + + active_cases: int | None = None + + reason: str | None = None + + +class CaseRoutingPreviewMatchedRules(BaseModel): + uuid: str | None = None + + name: str | None = None + + priority: int | None = None + + +class CaseRoutingPreview(BaseModel): + rule_uuid: str | None = None + + rule_name: str | None = None + + rule_version: int | None = None + + blueprint_uuid: str | None = None + + team_uuid: str | None = None + + assignee_user_id: str | None = None + + reason: str | None = None + + candidates: list[CaseRoutingPreviewCandidates] | None = None + + matched_rules: list[CaseRoutingPreviewMatchedRules] | None = None diff --git a/src/didit/types/components/case_routing_rule.py b/src/didit/types/components/case_routing_rule.py new file mode 100644 index 0000000..165e85e --- /dev/null +++ b/src/didit/types/components/case_routing_rule.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.case_routing_condition import CaseRoutingCondition + +__all__ = ["CaseRoutingRule"] + + +class CaseRoutingRule(BaseModel): + name: str | None = None + + description: str | None = None + + priority: int | None = None + + conditions: list[CaseRoutingCondition] | None = None + + required_skills: list[str] | None = None + + blueprint: str | None = None + + team: str | None = None + + is_active: bool | None = None + + uuid: str | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + version: int | None = None + + blueprint_name: str | None = None + + team_name: str | None = None diff --git a/src/didit/types/components/case_routing_rule_create.py b/src/didit/types/components/case_routing_rule_create.py new file mode 100644 index 0000000..122f2b9 --- /dev/null +++ b/src/didit/types/components/case_routing_rule_create.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.case_routing_condition import CaseRoutingConditionParam + +__all__ = ["CaseRoutingRuleCreate"] + + +class CaseRoutingRuleCreate(TypedDict, total=False): + name: Required[str] + + description: str + + priority: int + + conditions: Required[Sequence[CaseRoutingConditionParam]] + + required_skills: Sequence[str] + + blueprint: Required[str] + + team: str | None + + is_active: bool + + publish: bool + """Set false to save a never-published draft. The record remains inactive at version 0 until the draft is explicitly published. Omitting this field preserves immediate creation for existing integrations.""" diff --git a/src/didit/types/components/case_subject.py b/src/didit/types/components/case_subject.py new file mode 100644 index 0000000..0b9b366 --- /dev/null +++ b/src/didit/types/components/case_subject.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["CaseSubject"] + + +class CaseSubject(BaseModel): + """The single user or business the case is anchored to.""" + + type: Literal["user", "business"] | str | None = None + + uuid: str | None = None + + name: str | None = None + + external_id: str | None = None + """The subject's vendor_data.""" diff --git a/src/didit/types/components/customer_messaging_case_request.py b/src/didit/types/components/customer_messaging_case_request.py new file mode 100644 index 0000000..6c1238c --- /dev/null +++ b/src/didit/types/components/customer_messaging_case_request.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CustomerMessagingCaseRequest"] + + +class CustomerMessagingCaseRequest(BaseModel): + uuid: str + + purpose: str + + language: str + + recipient: str + + expires_at: _dt.datetime + + actor_email: str | None + + delivery_status: str + + delivered_at: _dt.datetime | None + + cancelled_at: _dt.datetime | None + + responded_at: _dt.datetime | None + + response: object + + has_attachment: bool + + created_at: _dt.datetime diff --git a/src/didit/types/components/customer_messaging_case_request_contact.py b/src/didit/types/components/customer_messaging_case_request_contact.py new file mode 100644 index 0000000..3e66033 --- /dev/null +++ b/src/didit/types/components/customer_messaging_case_request_contact.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CustomerMessagingCaseRequestContact"] + + +class CustomerMessagingCaseRequestContact(BaseModel): + uuid: str + + email: str + + verified_at: _dt.datetime diff --git a/src/didit/types/components/customer_messaging_case_request_options.py b/src/didit/types/components/customer_messaging_case_request_options.py new file mode 100644 index 0000000..62d4016 --- /dev/null +++ b/src/didit/types/components/customer_messaging_case_request_options.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.customer_messaging_case_request_contact import ( + CustomerMessagingCaseRequestContact, +) +from didit.types.components.customer_messaging_languages_enum import CustomerMessagingLanguagesEnum +from didit.types.components.customer_messaging_purposes_enum import CustomerMessagingPurposesEnum + +__all__ = ["CustomerMessagingCaseRequestOptions"] + + +class CustomerMessagingCaseRequestOptions(BaseModel): + contacts: list[CustomerMessagingCaseRequestContact] + + purposes: list[CustomerMessagingPurposesEnum] + + languages: list[CustomerMessagingLanguagesEnum] diff --git a/src/didit/types/components/customer_messaging_case_request_preview_response.py b/src/didit/types/components/customer_messaging_case_request_preview_response.py new file mode 100644 index 0000000..ff49919 --- /dev/null +++ b/src/didit/types/components/customer_messaging_case_request_preview_response.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["CustomerMessagingCaseRequestPreviewResponse"] + + +class CustomerMessagingCaseRequestPreviewResponse(BaseModel): + request_id: str + + expires_at: _dt.datetime + + recipient: str + + sender: str + + subject: str + + html: str + + requires_user_action: bool + + preview_token: str diff --git a/src/didit/types/components/customer_messaging_language_enum.py b/src/didit/types/components/customer_messaging_language_enum.py new file mode 100644 index 0000000..0e9c2a5 --- /dev/null +++ b/src/didit/types/components/customer_messaging_language_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CustomerMessagingLanguageEnum"] + + +CustomerMessagingLanguageEnum: TypeAlias = Literal["en", "es", "pt"] | str +"""* `en` - en +* `es` - es +* `pt` - pt +""" diff --git a/src/didit/types/components/customer_messaging_languages_enum.py b/src/didit/types/components/customer_messaging_languages_enum.py new file mode 100644 index 0000000..ac84335 --- /dev/null +++ b/src/didit/types/components/customer_messaging_languages_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CustomerMessagingLanguagesEnum"] + + +CustomerMessagingLanguagesEnum: TypeAlias = Literal["en", "es", "pt"] | str +"""* `en` - en +* `es` - es +* `pt` - pt +""" diff --git a/src/didit/types/components/customer_messaging_paginated_case_request_list.py b/src/didit/types/components/customer_messaging_paginated_case_request_list.py new file mode 100644 index 0000000..a3b1655 --- /dev/null +++ b/src/didit/types/components/customer_messaging_paginated_case_request_list.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.customer_messaging_case_request import CustomerMessagingCaseRequest + +__all__ = ["CustomerMessagingPaginatedCaseRequestList"] + + +class CustomerMessagingPaginatedCaseRequestList(BaseModel): + count: int + + next: str | None = None + + previous: str | None = None + + results: list[CustomerMessagingCaseRequest] diff --git a/src/didit/types/components/customer_messaging_purpose_enum.py b/src/didit/types/components/customer_messaging_purpose_enum.py new file mode 100644 index 0000000..5a20928 --- /dev/null +++ b/src/didit/types/components/customer_messaging_purpose_enum.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CustomerMessagingPurposeEnum"] + + +CustomerMessagingPurposeEnum: TypeAlias = ( + Literal[ + "identity_document", + "proof_of_address", + "business_document", + "confirm_details", + "review_in_progress", + ] + | str +) +"""* `identity_document` - identity_document +* `proof_of_address` - proof_of_address +* `business_document` - business_document +* `confirm_details` - confirm_details +* `review_in_progress` - review_in_progress +""" diff --git a/src/didit/types/components/customer_messaging_purposes_enum.py b/src/didit/types/components/customer_messaging_purposes_enum.py new file mode 100644 index 0000000..ba2fcd3 --- /dev/null +++ b/src/didit/types/components/customer_messaging_purposes_enum.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["CustomerMessagingPurposesEnum"] + + +CustomerMessagingPurposesEnum: TypeAlias = ( + Literal[ + "identity_document", + "proof_of_address", + "business_document", + "confirm_details", + "review_in_progress", + ] + | str +) +"""* `identity_document` - identity_document +* `proof_of_address` - proof_of_address +* `business_document` - business_document +* `confirm_details` - confirm_details +* `review_in_progress` - review_in_progress +""" diff --git a/src/didit/types/components/digital_id_wallet_api_audkenni_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_audkenni_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..42250bf --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_audkenni_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum: TypeAlias = Literal["ISL"] | str +"""* `ISL` - ISL""" diff --git a/src/didit/types/components/digital_id_wallet_api_connectid_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_connectid_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..7d7977b --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_connectid_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum: TypeAlias = Literal["AUS"] | str +"""* `AUS` - AUS""" diff --git a/src/didit/types/components/digital_id_wallet_api_country520_enum.py b/src/didit/types/components/digital_id_wallet_api_country520_enum.py new file mode 100644 index 0000000..0581c77 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_country520_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiCountry520Enum"] + + +DigitalIdWalletApiCountry520Enum: TypeAlias = Literal["GBR"] | str +"""* `GBR` - GBR""" diff --git a/src/didit/types/components/digital_id_wallet_api_country_afc_enum.py b/src/didit/types/components/digital_id_wallet_api_country_afc_enum.py new file mode 100644 index 0000000..ee6bda5 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_country_afc_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiCountryAfcEnum"] + + +DigitalIdWalletApiCountryAfcEnum: TypeAlias = Literal["NOR"] | str +"""* `NOR` - NOR""" diff --git a/src/didit/types/components/digital_id_wallet_api_country_e2d_enum.py b/src/didit/types/components/digital_id_wallet_api_country_e2d_enum.py new file mode 100644 index 0000000..6ffabe3 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_country_e2d_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiCountryE2dEnum"] + + +DigitalIdWalletApiCountryE2dEnum: TypeAlias = Literal["CZE"] | str +"""* `CZE` - CZE""" diff --git a/src/didit/types/components/digital_id_wallet_api_country_ee9_enum.py b/src/didit/types/components/digital_id_wallet_api_country_ee9_enum.py new file mode 100644 index 0000000..9a63990 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_country_ee9_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiCountryEe9Enum"] + + +DigitalIdWalletApiCountryEe9Enum: TypeAlias = Literal["SWE"] | str +"""* `SWE` - SWE""" diff --git a/src/didit/types/components/digital_id_wallet_api_device_enum.py b/src/didit/types/components/digital_id_wallet_api_device_enum.py new file mode 100644 index 0000000..ec67f0c --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_device_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiDeviceEnum"] + + +DigitalIdWalletApiDeviceEnum: TypeAlias = Literal["same_device", "cross_device"] | str +"""* `same_device` - same_device +* `cross_device` - cross_device +""" diff --git a/src/didit/types/components/digital_id_wallet_api_diia_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_diia_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..d5571ec --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_diia_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum: TypeAlias = Literal["UKR"] | str +"""* `UKR` - UKR""" diff --git a/src/didit/types/components/digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..6e4302f --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum: TypeAlias = ( + Literal["LVA"] | str +) +"""* `LVA` - LVA""" diff --git a/src/didit/types/components/digital_id_wallet_api_eudi_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_eudi_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..4f7846b --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_eudi_wallet_verification_start_country_enum.py @@ -0,0 +1,76 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiEudiWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiEudiWalletVerificationStartCountryEnum: TypeAlias = ( + Literal[ + "AUT", + "BEL", + "BGR", + "HRV", + "CYP", + "CZE", + "DNK", + "EST", + "FIN", + "FRA", + "DEU", + "GRC", + "HUN", + "IRL", + "ITA", + "LVA", + "LTU", + "LUX", + "MLT", + "NLD", + "POL", + "PRT", + "ROU", + "SVK", + "SVN", + "ESP", + "SWE", + "ISL", + "LIE", + "NOR", + ] + | str +) +"""* `AUT` - AUT +* `BEL` - BEL +* `BGR` - BGR +* `HRV` - HRV +* `CYP` - CYP +* `CZE` - CZE +* `DNK` - DNK +* `EST` - EST +* `FIN` - FIN +* `FRA` - FRA +* `DEU` - DEU +* `GRC` - GRC +* `HUN` - HUN +* `IRL` - IRL +* `ITA` - ITA +* `LVA` - LVA +* `LTU` - LTU +* `LUX` - LUX +* `MLT` - MLT +* `NLD` - NLD +* `POL` - POL +* `PRT` - PRT +* `ROU` - ROU +* `SVK` - SVK +* `SVN` - SVN +* `ESP` - ESP +* `SWE` - SWE +* `ISL` - ISL +* `LIE` - LIE +* `NOR` - NOR +""" diff --git a/src/didit/types/components/digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..d3ff8e7 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum: TypeAlias = Literal["FRA"] | str +"""* `FRA` - FRA""" diff --git a/src/didit/types/components/digital_id_wallet_api_ftn_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_ftn_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..4ef8ff1 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_ftn_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiFtnWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiFtnWalletVerificationStartCountryEnum: TypeAlias = Literal["FIN"] | str +"""* `FIN` - FIN""" diff --git a/src/didit/types/components/digital_id_wallet_api_govbr_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_govbr_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..b3669e4 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_govbr_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum: TypeAlias = Literal["BRA"] | str +"""* `BRA` - BRA""" diff --git a/src/didit/types/components/digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..7e77db1 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum: TypeAlias = Literal["EST"] | str +"""* `EST` - EST""" diff --git a/src/didit/types/components/digital_id_wallet_api_idin_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_idin_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..421ab6e --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_idin_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiIdinWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiIdinWalletVerificationStartCountryEnum: TypeAlias = Literal["NLD"] | str +"""* `NLD` - NLD""" diff --git a/src/didit/types/components/digital_id_wallet_api_interaction_enum.py b/src/didit/types/components/digital_id_wallet_api_interaction_enum.py new file mode 100644 index 0000000..b8ee1fc --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_interaction_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiInteractionEnum"] + + +DigitalIdWalletApiInteractionEnum: TypeAlias = Literal["redirect", "in_app"] | str +"""* `redirect` - redirect +* `in_app` - in_app +""" diff --git a/src/didit/types/components/digital_id_wallet_api_itsme_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_itsme_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..7cfaac6 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_itsme_wallet_verification_start_country_enum.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum: TypeAlias = ( + Literal["BEL", "LUX", "NLD"] | str +) +"""* `BEL` - BEL +* `LUX` - LUX +* `NLD` - NLD +""" diff --git a/src/didit/types/components/digital_id_wallet_api_mitid_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_mitid_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..6a89974 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_mitid_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiMitidWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiMitidWalletVerificationStartCountryEnum: TypeAlias = Literal["DNK"] | str +"""* `DNK` - DNK""" diff --git a/src/didit/types/components/digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..bb421b6 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum: TypeAlias = ( + Literal["EST", "LTU"] | str +) +"""* `EST` - EST +* `LTU` - LTU +""" diff --git a/src/didit/types/components/digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..c6b8646 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum: TypeAlias = ( + Literal["DEU"] | str +) +"""* `DEU` - DEU""" diff --git a/src/didit/types/components/digital_id_wallet_api_smart_id_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_smart_id_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..5c14c58 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_smart_id_wallet_verification_start_country_enum.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum: TypeAlias = ( + Literal["EST", "LVA", "LTU", "BEL"] | str +) +"""* `EST` - EST +* `LVA` - LVA +* `LTU` - LTU +* `BEL` - BEL +""" diff --git a/src/didit/types/components/digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum.py b/src/didit/types/components/digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum.py new file mode 100644 index 0000000..86d81fe --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum"] + + +DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum: TypeAlias = Literal["ARE"] | str +"""* `ARE` - ARE""" diff --git a/src/didit/types/components/digital_id_wallet_api_wallet.py b/src/didit/types/components/digital_id_wallet_api_wallet.py new file mode 100644 index 0000000..e668d88 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["DigitalIdWalletApiWallet"] + + +class DigitalIdWalletApiWallet(BaseModel): + id: str + """Catalog id of the wallet, e.g. bankid_se.""" + + name: str + + issuing_authority: str | None diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_catalog_entry.py b/src/didit/types/components/digital_id_wallet_api_wallet_catalog_entry.py new file mode 100644 index 0000000..d09c165 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_catalog_entry.py @@ -0,0 +1,34 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.digital_id_wallet_api_interaction_enum import ( + DigitalIdWalletApiInteractionEnum, +) + +__all__ = ["DigitalIdWalletApiWalletCatalogEntry"] + + +class DigitalIdWalletApiWalletCatalogEntry(BaseModel): + id: str + + name: str + + path: str + """The wallet's own start endpoint.""" + + countries: list[str] + + issuing_authority: str + + level_of_assurance: str | None + + interaction: DigitalIdWalletApiInteractionEnum + """* `redirect` - redirect + * `in_app` - in_app + """ + + required_fields: list[str] + + available: bool + """Whether this application can start it in this environment now. Listed does not mean live.""" diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_catalog_response.py b/src/didit/types/components/digital_id_wallet_api_wallet_catalog_response.py new file mode 100644 index 0000000..729be7d --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_catalog_response.py @@ -0,0 +1,13 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.digital_id_wallet_api_wallet_catalog_entry import ( + DigitalIdWalletApiWalletCatalogEntry, +) + +__all__ = ["DigitalIdWalletApiWalletCatalogResponse"] + + +class DigitalIdWalletApiWalletCatalogResponse(BaseModel): + wallets: list[DigitalIdWalletApiWalletCatalogEntry] diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_identity.py b/src/didit/types/components/digital_id_wallet_api_wallet_identity.py new file mode 100644 index 0000000..a3c54e3 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_identity.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["DigitalIdWalletApiWalletIdentity"] + + +class DigitalIdWalletApiWalletIdentity(BaseModel): + full_name: str | None + + first_name: str | None + + last_name: str | None + + date_of_birth: _dt.date | None + + nationality: str | None + """ISO3 nationality, only when the wallet asserts one (never inferred).""" + + issuing_state: str + """ISO3 country whose scheme issued the identity.""" diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_next_action.py b/src/didit/types/components/digital_id_wallet_api_wallet_next_action.py new file mode 100644 index 0000000..0821f9f --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_next_action.py @@ -0,0 +1,27 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.digital_id_wallet_api_wallet_next_action_type_enum import ( + DigitalIdWalletApiWalletNextActionTypeEnum, +) + +__all__ = ["DigitalIdWalletApiWalletNextAction"] + + +class DigitalIdWalletApiWalletNextAction(BaseModel): + type: DigitalIdWalletApiWalletNextActionTypeEnum + """redirect: send the user's browser to url. confirm_in_app: show verification_code to the user, who checks it matches the code in their wallet app and confirms there; then poll. + + * `redirect` - redirect + * `confirm_in_app` - confirm_in_app + """ + + url: str | None = None + """redirect only: where the user signs in with the wallet.""" + + verification_code: str | None = None + """confirm_in_app only: the 4-digit comparison code the wallet app shows too.""" + + poll_interval_seconds: int | None = None + """confirm_in_app only: how often to call POST .../verifications/{request_id}/poll/ for prompt feedback. Didit also polls pending sign-ins in the background, so the webhook reports the outcome even when you do not poll.""" diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_next_action_type_enum.py b/src/didit/types/components/digital_id_wallet_api_wallet_next_action_type_enum.py new file mode 100644 index 0000000..dae7b0a --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_next_action_type_enum.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiWalletNextActionTypeEnum"] + + +DigitalIdWalletApiWalletNextActionTypeEnum: TypeAlias = Literal["redirect", "confirm_in_app"] | str +"""* `redirect` - redirect +* `confirm_in_app` - confirm_in_app +""" diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_verification_response.py b/src/didit/types/components/digital_id_wallet_api_wallet_verification_response.py new file mode 100644 index 0000000..e50d83e --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_verification_response.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.digital_id_wallet_api_wallet_verification_result import ( + DigitalIdWalletApiWalletVerificationResult, +) + +__all__ = ["DigitalIdWalletApiWalletVerificationResponse"] + + +class DigitalIdWalletApiWalletVerificationResponse(BaseModel): + """One standalone wallet verification, as every wallet endpoint answers it.""" + + request_id: str + + wallet_verification: DigitalIdWalletApiWalletVerificationResult + + vendor_data: str | None + + metadata: dict[str, object] | None + """The metadata object sent at start, or null.""" + + created_at: _dt.datetime diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_verification_result.py b/src/didit/types/components/digital_id_wallet_api_wallet_verification_result.py new file mode 100644 index 0000000..911c8c4 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_verification_result.py @@ -0,0 +1,61 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.digital_id_wallet_api_interaction_enum import ( + DigitalIdWalletApiInteractionEnum, +) +from didit.types.components.digital_id_wallet_api_wallet import DigitalIdWalletApiWallet +from didit.types.components.digital_id_wallet_api_wallet_identity import ( + DigitalIdWalletApiWalletIdentity, +) +from didit.types.components.digital_id_wallet_api_wallet_next_action import ( + DigitalIdWalletApiWalletNextAction, +) +from didit.types.components.digital_id_wallet_api_wallet_verification_result_outcome_enum import ( + DigitalIdWalletApiWalletVerificationResultOutcomeEnum, +) + +__all__ = ["DigitalIdWalletApiWalletVerificationResult"] + + +class DigitalIdWalletApiWalletVerificationResult(BaseModel): + wallet: DigitalIdWalletApiWallet + + country: str + + status: str + """Not Finished while pending, then Approved (verified) or Declined.""" + + outcome: DigitalIdWalletApiWalletVerificationResultOutcomeEnum + """* `pending` - Pending + * `verified` - Verified + * `cancelled` - Cancelled by the user + * `timeout` - Timed out + * `failed` - Failed + """ + + error: str | None + + interaction: DigitalIdWalletApiInteractionEnum + + simulated: bool + + expires_at: _dt.datetime + + next_action: DigitalIdWalletApiWalletNextAction | None + + identity: DigitalIdWalletApiWalletIdentity | None + + attributes: dict[str, object] | None + + level_of_assurance: str | None + + verified_at: _dt.datetime | None + + signature_valid: bool | None + """True once the credential's signature was verified; null for a simulated one.""" + + credential_type: str | None diff --git a/src/didit/types/components/digital_id_wallet_api_wallet_verification_result_outcome_enum.py b/src/didit/types/components/digital_id_wallet_api_wallet_verification_result_outcome_enum.py new file mode 100644 index 0000000..a649474 --- /dev/null +++ b/src/didit/types/components/digital_id_wallet_api_wallet_verification_result_outcome_enum.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["DigitalIdWalletApiWalletVerificationResultOutcomeEnum"] + + +DigitalIdWalletApiWalletVerificationResultOutcomeEnum: TypeAlias = ( + Literal["pending", "verified", "cancelled", "timeout", "failed"] | str +) +"""* `pending` - Pending +* `verified` - Verified +* `cancelled` - Cancelled by the user +* `timeout` - Timed out +* `failed` - Failed +""" diff --git a/src/didit/types/components/document_ai_detected_code.py b/src/didit/types/components/document_ai_detected_code.py new file mode 100644 index 0000000..08a0625 --- /dev/null +++ b/src/didit/types/components/document_ai_detected_code.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["DocumentAIDetectedCode"] + + +class DocumentAIDetectedCode(BaseModel): + type: str + """Detected symbology, for example `QR_CODE`, `CODE_128`, `EAN_13`, `DATA_MATRIX`, or `PDF_417`.""" + + page_number: int + """1-based page number.""" + + position: list[list[int]] | None + """Detected polygon in rendered-page pixel coordinates. `null` when no reliable location is available.""" + + x: int | None + """Left edge of the bounding box in rendered-page pixels.""" + + y: int | None + """Top edge of the bounding box in rendered-page pixels.""" + + width: int | None + """Bounding-box width in rendered-page pixels.""" + + height: int | None + """Bounding-box height in rendered-page pixels.""" + + page_width: int + """Rendered page width in pixels.""" + + page_height: int + """Rendered page height in pixels.""" + + raw_payload: str + """Raw decoder payload as a base64 string when available; otherwise an empty string.""" + + parsed_payload: object + """Decoded string or structured value. `null` when the code was located but could not be decoded.""" diff --git a/src/didit/types/components/email_intelligence.py b/src/didit/types/components/email_intelligence.py new file mode 100644 index 0000000..268b3ea --- /dev/null +++ b/src/didit/types/components/email_intelligence.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["EmailIntelligence"] + + +class EmailIntelligence(BaseModel): + """Normalized email-intelligence block. Present only when the intelligence add-on ran for this email; when present it is also exposed as the `enrichment` alias with identical values.""" + + score: float | None = None + """Normalized 0-100 risk score. Higher is riskier.""" + + deliverable: str | None = None + """Deliverability band (e.g., `high`).""" + + breach_risk_level: str | None = None + """`Low`, `Medium` or `High`.""" + + breach_risk_index: float | None = None + """Numeric breach-risk index.""" + + is_recent_breach: bool | None = None + """`true` when the address appears in a recent breach.""" + + is_frequent_breached: bool | None = None + """`true` when the address appears in many breaches.""" + + is_disposable: bool | None = None + """`true` when the domain is a throwaway provider.""" + + is_free_domain: bool | None = None + """`true` when the domain is a free mail provider.""" + + is_breached: bool | None = None + """`true` when the address appears in known breaches.""" + + no_of_breaches: float | None = None + """Number of known breaches containing the address.""" + + reason_codes: list[str] | None = None + """Machine-readable codes explaining the score. Values can grow over time, so do not reject unknown codes.""" diff --git a/src/didit/types/components/email_match_item.py b/src/didit/types/components/email_match_item.py new file mode 100644 index 0000000..0d5d951 --- /dev/null +++ b/src/didit/types/components/email_match_item.py @@ -0,0 +1,37 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["EmailMatchItem"] + + +class EmailMatchItem(BaseModel): + """Another session of your application that used the same email address with a different `vendor_data`.""" + + session_id: str | None = None + """Session id of the other verification that used this email address.""" + + session_number: int | None = None + """Human-friendly sequential number of that session.""" + + vendor_data: str | None = None + """`vendor_data` of that session.""" + + verification_date: str | None = None + """When that session was created (`YYYY-MM-DDTHH:MM:SSZ`).""" + + email: str | None = None + """The matched email address.""" + + status: str | None = None + """Status of that session (e.g., `Approved`, `Declined`, `In Progress`).""" + + is_blocklisted: bool | None = None + """Whether the email address is on your application's blocklist.""" + + api_service: str | None = None + """For API-type sessions, the standalone API that created it; `null` for regular verification sessions.""" + + source: str | None = None + """Where the match was found. Usually `session`; a blocklist hit via an organization list entry uses `list_entry`.""" diff --git a/src/didit/types/components/email_risk_response.py b/src/didit/types/components/email_risk_response.py new file mode 100644 index 0000000..6c9e48b --- /dev/null +++ b/src/didit/types/components/email_risk_response.py @@ -0,0 +1,106 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.email_intelligence import EmailIntelligence +from didit.types.components.email_match_item import EmailMatchItem +from didit.types.components.email_social_footprint import EmailSocialFootprint +from didit.types.components.risk_lifecycle_event import RiskLifecycleEvent +from didit.types.components.risk_warning import RiskWarning + +__all__ = ["EmailRiskResponse", "EmailRiskResponseEmail", "EmailRiskResponseEmailBreaches"] + + +class EmailRiskResponseEmailBreaches(BaseModel): + name: str | None = None + """Breach identifier.""" + + domain: str | None = None + """Domain of the breached service.""" + + breach_date: _dt.date | None = None + """Date the breach occurred.""" + + breach_emails_count: int | None = None + """Total number of accounts exposed in the breach.""" + + description: str | None = None + """Human-readable description of the breach (may contain HTML markup).""" + + logo_path: str | None = None + """URL of the breached service's logo.""" + + data_classes: list[str] | None = None + """Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`).""" + + is_verified: bool | None = None + """Whether the breach has been verified as legitimate.""" + + +class EmailRiskResponseEmail(BaseModel): + """Normalized email report, the same block used by Email Verification responses.""" + + status: Literal["Approved", "In Review", "Declined"] | str + """Decision for this email address. Mirrors the top-level `status`.""" + + email: str + """The scored address.""" + + is_breached: bool + """Whether the address appears in a known credential breach.""" + + breaches: list[EmailRiskResponseEmailBreaches] + """Up to 5 most recent known breaches containing this address. Empty array when `is_breached` is `false`.""" + + is_disposable: bool + """Whether the domain belongs to a throwaway provider.""" + + is_undeliverable: bool + """Whether the address was found undeliverable.""" + + verification_attempts: int | None = None + """OTP sends for this address. `0` for risk lookups.""" + + verified_at: _dt.datetime | None = None + """When the address was last verified by an OTP. `null` for risk lookups.""" + + warnings: list[RiskWarning] | None = None + """Risk warnings recorded for this request. Empty array when there are none.""" + + lifecycle: list[RiskLifecycleEvent] | None = None + """Chronological audit trail of the request. Empty array for risk lookups without sends.""" + + matches: list[EmailMatchItem] | None = None + """Other sessions of your application where the same email address was used by a different user (different `vendor_data`). Up to 5 entries. Empty array when there are no matches.""" + + email_intelligence: EmailIntelligence | None = None + """Normalized email-intelligence block. Present only when the intelligence add-on ran for this email; when present it is also exposed as the `enrichment` alias with identical values.""" + + enrichment: EmailIntelligence | None = None + """Normalized email-intelligence block. Present only when the intelligence add-on ran for this email; when present it is also exposed as the `enrichment` alias with identical values.""" + + email_social: EmailSocialFootprint | None = None + """Social footprint of the email address: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the email address was found on none of them - a valid, billable answer and a common synthetic-identity signal.""" + + +class EmailRiskResponse(BaseModel): + request_id: str + """Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity.""" + + status: Literal["Approved", "In Review", "Declined"] | str + """Final decision for this API session. Normally `Approved`; the risk evaluation can produce `In Review` or `Declined`.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, `null` when you did not send one.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent, `null` when you did not send one.""" + + created_at: _dt.datetime + """Timestamp of this response.""" + + email: EmailRiskResponseEmail | None = None + """Normalized email report, the same block used by Email Verification responses.""" diff --git a/src/didit/types/components/email_social_footprint.py b/src/didit/types/components/email_social_footprint.py new file mode 100644 index 0000000..80b0c73 --- /dev/null +++ b/src/didit/types/components/email_social_footprint.py @@ -0,0 +1,68 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.social_footprint_category_summary import SocialFootprintCategorySummary +from didit.types.components.social_footprint_platform import SocialFootprintPlatform +from didit.types.components.social_footprint_profile import SocialFootprintProfile + +__all__ = ["EmailSocialFootprint"] + + +class EmailSocialFootprint(BaseModel): + """Social footprint of the email address: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the email address was found on none of them - a valid, billable answer and a common synthetic-identity signal.""" + + profiles_registered: int | None = None + """Platforms where the email address is confirmed registered.""" + + profiles_checked: int | None = None + """Platforms evaluated for registration.""" + + names_found: int | None = None + """Platforms that returned an associated name.""" + + photos_found: int | None = None + """Platforms that returned a profile photo.""" + + communication_breadth: int | None = None + """Communication platforms with detected presence.""" + + digital_footprint_diversity: int | None = None + """Distinct platform categories with detected presence.""" + + digital_sophistication_index: int | None = None + """Composite digital-maturity indicator, 0-10.""" + + engagement_depth_index: int | None = None + """Profile-richness indicator, 0-10.""" + + subscription_affordability_index: int | None = None + """Free vs paid platform mix, 0-10.""" + + free_platforms: int | None = None + """Registrations on free platforms.""" + + full_paid_platforms: int | None = None + """Registrations on fully paid platforms.""" + + partial_paid_platforms: int | None = None + """Registrations on freemium platforms.""" + + whatsapp_profile_completeness: ( + Literal["Basic", "Partial", "Complete", "Not Present"] | str | None + ) = None + """Completeness of the WhatsApp profile. Phone only; null for email.""" + + registered_platforms: list[SocialFootprintPlatform] | None = None + """Platforms where registration is confirmed.""" + + not_registered_platforms: list[SocialFootprintPlatform] | None = None + """Platforms checked with a negative result.""" + + categories: dict[str, SocialFootprintCategorySummary] | None = None + """Registered count and ratio per platform category.""" + + profiles: dict[str, SocialFootprintProfile] | None = None + """Per-platform profile details, keyed by platform, when the platform exposes them.""" diff --git a/src/didit/types/components/email_verification_check_response.py b/src/didit/types/components/email_verification_check_response.py new file mode 100644 index 0000000..1b9f6e1 --- /dev/null +++ b/src/didit/types/components/email_verification_check_response.py @@ -0,0 +1,171 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.email_social_footprint import EmailSocialFootprint + +__all__ = [ + "EmailVerificationCheckResponse", + "EmailVerificationCheckResponseEmail", + "EmailVerificationCheckResponseEmailBreaches", + "EmailVerificationCheckResponseEmailLifecycle", + "EmailVerificationCheckResponseEmailMatches", + "EmailVerificationCheckResponseEmailWarnings", +] + + +class EmailVerificationCheckResponseEmailBreaches(BaseModel): + name: str | None = None + """Breach identifier.""" + + domain: str | None = None + """Domain of the breached service.""" + + breach_date: _dt.date | None = None + """Date the breach occurred.""" + + breach_emails_count: int | None = None + """Total number of accounts exposed in the breach.""" + + description: str | None = None + """Human-readable description of the breach (may contain HTML markup).""" + + logo_path: str | None = None + """URL of the breached service's logo.""" + + data_classes: list[str] | None = None + """Categories of data exposed, in snake_case (e.g., `email_addresses`, `passwords`).""" + + is_verified: bool | None = None + """Whether the breach has been verified as legitimate.""" + + +class EmailVerificationCheckResponseEmailLifecycle(BaseModel): + type: str | None = None + """Event type.""" + + timestamp: _dt.datetime | None = None + """When the event happened.""" + + details: dict[str, object] | None = None + """Event-specific payload. Send events: `{status, reason}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review.""" + + fee: float | None = None + """USD amount billed for this event. Non-zero only on the first successful send; 0 for retries, code entries, and status events.""" + + +class EmailVerificationCheckResponseEmailMatches(BaseModel): + session_id: str | None = None + """Session id of the other verification that used this email address.""" + + session_number: int | None = None + """Human-friendly sequential number of that session.""" + + vendor_data: str | None = None + """`vendor_data` of that session.""" + + verification_date: str | None = None + """When that session was created (`YYYY-MM-DDTHH:MM:SSZ`).""" + + email: str | None = None + """The matched email address.""" + + status: str | None = None + """Status of that session (e.g., `Approved`, `Declined`, `In Progress`).""" + + is_blocklisted: bool | None = None + """Whether the email address is on your application's blocklist.""" + + api_service: str | None = None + """For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions.""" + + source: str | None = None + """Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: "list_entry"` and null `session_id`/`session_number`.""" + + +class EmailVerificationCheckResponseEmailWarnings(BaseModel): + feature: str | None = None + """Feature that produced the warning. Always `EMAIL` here.""" + + risk: str | None = None + """Machine-readable risk code.""" + + additional_data: dict[str, object] | None = None + """Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status.""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class EmailVerificationCheckResponseEmail(BaseModel): + """Full email report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.""" + + status: Literal["Approved", "Declined", "In Review", "Not Finished", "Expired"] | str | None = ( + None + ) + """Final status of the email verification. Matches the top-level `status` for finalized checks.""" + + email: str | None = None + """The verified email address.""" + + is_breached: bool | None = None + """`true` when the address appears in known data breaches (breach intelligence lookup run at finalization).""" + + breaches: list[EmailVerificationCheckResponseEmailBreaches] | None = None + """Up to 5 most recent known breaches containing this address. Empty array (or omitted) when `is_breached` is `false`.""" + + is_disposable: bool | None = None + """`true` when the domain belongs to a known disposable/temporary-mail provider.""" + + is_undeliverable: bool | None = None + """`true` when the address cannot receive mail (failed syntax or DNS/MX validation, or the message bounced).""" + + verification_attempts: int | None = None + """Number of OTP sends for this verification (1, or 2 after a retry).""" + + verified_at: _dt.datetime | None = None + """When the correct code was entered. `null` if the verification was never approved.""" + + warnings: list[EmailVerificationCheckResponseEmailWarnings] | None = None + """Risk warnings recorded during the verification (duplicate, breached, disposable, undeliverable, attempts exceeded, blocklist, etc.). Empty array when there are none.""" + + lifecycle: list[EmailVerificationCheckResponseEmailLifecycle] | None = None + """Chronological audit trail of the verification. Event `type` is one of `EMAIL_VERIFICATION_MESSAGE_SENT`, `EMAIL_VERIFICATION_RETRY_MESSAGE_SENT`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `EMAIL_VERIFICATION_APPROVED`, `EMAIL_VERIFICATION_DECLINED`, `EMAIL_VERIFICATION_IN_REVIEW`, `EMAIL_VERIFICATION_EXPIRED`.""" + + matches: list[EmailVerificationCheckResponseEmailMatches] | None = None + """Other sessions of your application (KYC, KYB, or API) where the same email address was used by a different user (different `vendor_data`). Up to 5 entries; a synthetic list-entry blocklist match is prepended when present (and only when no session-derived blocklisted email exists) - session-derived matches keep created_at order. Empty array when there are no matches.""" + + email_social: EmailSocialFootprint | None = None + """Social footprint of the email address: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the email address was found on none of them - a valid, billable answer and a common synthetic-identity signal.""" + + +class EmailVerificationCheckResponse(BaseModel): + request_id: str | None = None + """On `Approved`/`Declined`: the session id of the matched verification - identical to the `request_id` returned by `POST /v3/email/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later.""" + + status: Literal["Approved", "Declined", "Failed", "Expired or Not Found"] | str | None = None + """`Approved` - correct code, no declining risk. `Declined` - terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` - wrong code, attempts remaining. `Expired or Not Found` - no pending verification for this address in the last 5 minutes.""" + + message: str | None = None + """Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code.""" + + email: EmailVerificationCheckResponseEmail | None = None + """Full email report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.""" + + vendor_data: str | None = None + """`vendor_data` of the matched verification's session. `null` on `Expired or Not Found`.""" + + metadata: dict[str, object] | None = None + """`metadata` of the matched verification's session. `null` on `Expired or Not Found`.""" + + created_at: _dt.datetime | None = None + """Timestamp of this check response.""" diff --git a/src/didit/types/components/email_verification_send_response.py b/src/didit/types/components/email_verification_send_response.py new file mode 100644 index 0000000..d765d13 --- /dev/null +++ b/src/didit/types/components/email_verification_send_response.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["EmailVerificationSendResponse"] + + +class EmailVerificationSendResponse(BaseModel): + request_id: str | None = None + """Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/email/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`.""" + + status: Literal["Success", "Retry", "Undeliverable"] | str | None = None + """`Success` - OTP emailed to a new verification (billed). `Retry` - fresh OTP emailed for the pending verification created by a previous send (free). `Undeliverable` - the address failed deliverability validation or the message could not be sent; the verification is immediately finalized as `Declined` and nothing is billed.""" + + reason: Literal["email_can_not_be_delivered"] | str | None = None + """`email_can_not_be_delivered` when `status` is `Undeliverable`; `null` otherwise.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` stored on the session (from the first send).""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` stored on the session (from the first send).""" diff --git a/src/didit/types/components/entity_network_membership.py b/src/didit/types/components/entity_network_membership.py new file mode 100644 index 0000000..13e09e9 --- /dev/null +++ b/src/didit/types/components/entity_network_membership.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_list_item import NetworkListItem + +__all__ = ["EntityNetworkMembership"] + + +class EntityNetworkMembership(BaseModel): + network: NetworkListItem | None = None + + subject: dict[str, object] | None = None + + signal_types: ( + list[ + Literal["ip_address", "device", "face", "document_number", "phone", "email", "address"] + | str + ] + | None + ) = None + + signal_count: int | None = None + + joined_at: _dt.datetime | None = None + + last_seen_at: _dt.datetime | None = None diff --git a/src/didit/types/components/fraud_availability_enum.py b/src/didit/types/components/fraud_availability_enum.py new file mode 100644 index 0000000..1537682 --- /dev/null +++ b/src/didit/types/components/fraud_availability_enum.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudAvailabilityEnum"] + + +FraudAvailabilityEnum: TypeAlias = ( + Literal[ + "available", + "not_requested", + "no_reference", + "insufficient_history", + "unsupported_region", + "source_unavailable", + ] + | str +) +"""* `available` - Available +* `not_requested` - Not Requested +* `no_reference` - No Reference +* `insufficient_history` - Insufficient History +* `unsupported_region` - Unsupported Region +* `source_unavailable` - Source Unavailable +""" diff --git a/src/didit/types/components/fraud_band_enum.py b/src/didit/types/components/fraud_band_enum.py new file mode 100644 index 0000000..7aa744f --- /dev/null +++ b/src/didit/types/components/fraud_band_enum.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudBandEnum"] + + +FraudBandEnum: TypeAlias = Literal["low", "medium", "high", "unknown"] | str +"""* `low` - Low +* `medium` - Medium +* `high` - High +* `unknown` - Unknown +""" diff --git a/src/didit/types/components/fraud_check_attributes_request.py b/src/didit/types/components/fraud_check_attributes_request.py new file mode 100644 index 0000000..930c67f --- /dev/null +++ b/src/didit/types/components/fraud_check_attributes_request.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from typing_extensions import TypedDict + +__all__ = ["FraudCheckAttributesRequest"] + + +class FraudCheckAttributesRequest(TypedDict, total=False): + """Corroborating attributes. All optional; they widen module coverage.""" + + first_name: str + + last_name: str + + full_name: str + + date_of_birth: str | _dt.date + + nationality: str + + gender: str + + address: str + + city: str + + state: str + + postal_code: str diff --git a/src/didit/types/components/fraud_check_context_request.py b/src/didit/types/components/fraud_check_context_request.py new file mode 100644 index 0000000..64466ad --- /dev/null +++ b/src/didit/types/components/fraud_check_context_request.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["FraudCheckContextRequest"] + + +class FraudCheckContextRequest(TypedDict, total=False): + ip_address: str + + user_agent: str + + country: str + """Country the subject claims to be in. Contextual only - never proof of identity ownership.""" + + purpose: str + + consent_obtained: bool diff --git a/src/didit/types/components/fraud_check_identifier_request.py b/src/didit/types/components/fraud_check_identifier_request.py new file mode 100644 index 0000000..ac47deb --- /dev/null +++ b/src/didit/types/components/fraud_check_identifier_request.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit.types.components.fraud_type_enum import FraudTypeEnum + +__all__ = ["FraudCheckIdentifierRequest"] + + +class FraudCheckIdentifierRequest(TypedDict, total=False): + type: Required[FraudTypeEnum] + """Identifier type. Determines which namespace fields are required. + + * `email` - Email + * `phone` - Phone + * `national_id` - National Id + * `tax_number` - Tax Number + * `passport_number` - Passport Number + * `driving_licence_number` - Driving Licence Number + * `residence_permit_number` - Residence Permit Number + * `voter_id` - Voter Id + * `external_id` - External Id + """ + + value: Required[str] + """The identifier value, in any customary format.""" + + country: str + """ISO-3166 alpha-2 or alpha-3 issuing country. Required for government identifier types: the same digits mean different people in different countries.""" + + issuer: str + """Optional issuing authority or sub-national issuer, when the country alone is not unique.""" diff --git a/src/didit/types/components/fraud_check_identity_relationship.py b/src/didit/types/components/fraud_check_identity_relationship.py new file mode 100644 index 0000000..016bb26 --- /dev/null +++ b/src/didit/types/components/fraud_check_identity_relationship.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.fraud_availability_enum import FraudAvailabilityEnum +from didit.types.components.fraud_result_enum import FraudResultEnum + +__all__ = ["FraudCheckIdentityRelationship"] + + +class FraudCheckIdentityRelationship(BaseModel): + result: FraudResultEnum + """* `match` - Match + * `mismatch` - Mismatch + * `inconclusive` - Inconclusive + * `no_reference` - No Reference + """ + + reference_type: str + + country: str + + availability: FraudAvailabilityEnum + """* `available` - Available + * `not_requested` - Not Requested + * `no_reference` - No Reference + * `insufficient_history` - Insufficient History + * `unsupported_region` - Unsupported Region + * `source_unavailable` - Source Unavailable + """ + + reason_codes: list[str] diff --git a/src/didit/types/components/fraud_check_input.py b/src/didit/types/components/fraud_check_input.py new file mode 100644 index 0000000..1112704 --- /dev/null +++ b/src/didit/types/components/fraud_check_input.py @@ -0,0 +1,40 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.fraud_availability_enum import FraudAvailabilityEnum +from didit.types.components.fraud_type_enum import FraudTypeEnum + +__all__ = ["FraudCheckInput"] + + +class FraudCheckInput(BaseModel): + reference: str + """Redacted handle for the submitted identifier.""" + + type: FraudTypeEnum + """* `email` - Email + * `phone` - Phone + * `national_id` - National Id + * `tax_number` - Tax Number + * `passport_number` - Passport Number + * `driving_licence_number` - Driving Licence Number + * `residence_permit_number` - Residence Permit Number + * `voter_id` - Voter Id + * `external_id` - External Id + """ + + namespace: dict[str, object] + + quality: str + + normalization: dict[str, object] + + availability: FraudAvailabilityEnum + """* `available` - Available + * `not_requested` - Not Requested + * `no_reference` - No Reference + * `insufficient_history` - Insufficient History + * `unsupported_region` - Unsupported Region + * `source_unavailable` - Source Unavailable + """ diff --git a/src/didit/types/components/fraud_check_module_report.py b/src/didit/types/components/fraud_check_module_report.py new file mode 100644 index 0000000..531b570 --- /dev/null +++ b/src/didit/types/components/fraud_check_module_report.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.fraud_exclude_enum import FraudExcludeEnum +from didit.types.components.fraud_status_enum import FraudStatusEnum + +__all__ = ["FraudCheckModuleReport"] + + +class FraudCheckModuleReport(BaseModel): + module: FraudExcludeEnum + """* `normalization` - Normalization + * `email_risk` - Email Risk + * `email_social` - Email Social + * `phone_social` - Phone Social + * `phone_risk` - Phone Risk + * `ip_risk` - Ip Risk + * `database_validation` - Database Validation + * `networks_same_org` - Networks Same Org + * `networks_cross_org` - Networks Cross Org + * `biometric_identity` - Biometric Identity + """ + + status: FraudStatusEnum + """* `planned` - Planned + * `completed` - Completed + * `skipped` - Skipped + * `failed` - Failed + """ + + reason: str | None + + latency_ms: int + + freshness: str | None + + billable_units: int diff --git a/src/didit/types/components/fraud_check_module_selection_request.py b/src/didit/types/components/fraud_check_module_selection_request.py new file mode 100644 index 0000000..f4c909e --- /dev/null +++ b/src/didit/types/components/fraud_check_module_selection_request.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +from didit.types.components.fraud_exclude_enum import FraudExcludeEnum + +__all__ = ["FraudCheckModuleSelectionRequest"] + + +class FraudCheckModuleSelectionRequest(TypedDict, total=False): + include: Sequence[FraudExcludeEnum] + + exclude: Sequence[FraudExcludeEnum] diff --git a/src/didit/types/components/fraud_check_plan.py b/src/didit/types/components/fraud_check_plan.py new file mode 100644 index 0000000..4a17852 --- /dev/null +++ b/src/didit/types/components/fraud_check_plan.py @@ -0,0 +1,23 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.fraud_check_planned_module import FraudCheckPlannedModule + +__all__ = ["FraudCheckPlan"] + + +class FraudCheckPlan(BaseModel): + profile_id: str + + profile_version: str + + modules: list[FraudCheckPlannedModule] + + estimated_price: str | None + + currency: str | None + + is_sandbox: bool + + can_afford: bool diff --git a/src/didit/types/components/fraud_check_planned_module.py b/src/didit/types/components/fraud_check_planned_module.py new file mode 100644 index 0000000..96681f7 --- /dev/null +++ b/src/didit/types/components/fraud_check_planned_module.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.fraud_exclude_enum import FraudExcludeEnum + +__all__ = ["FraudCheckPlannedModule"] + + +class FraudCheckPlannedModule(BaseModel): + module: FraudExcludeEnum + """* `normalization` - Normalization + * `email_risk` - Email Risk + * `email_social` - Email Social + * `phone_social` - Phone Social + * `phone_risk` - Phone Risk + * `ip_risk` - Ip Risk + * `database_validation` - Database Validation + * `networks_same_org` - Networks Same Org + * `networks_cross_org` - Networks Cross Org + * `biometric_identity` - Biometric Identity + """ + + available: bool + + availability_reason: str | None + + will_run: bool + + selection_reason: str | None + + estimated_price: str | None diff --git a/src/didit/types/components/fraud_check_response.py b/src/didit/types/components/fraud_check_response.py new file mode 100644 index 0000000..70b5a6e --- /dev/null +++ b/src/didit/types/components/fraud_check_response.py @@ -0,0 +1,78 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.fraud_check_identity_relationship import FraudCheckIdentityRelationship +from didit.types.components.fraud_check_input import FraudCheckInput +from didit.types.components.fraud_check_module_report import FraudCheckModuleReport +from didit.types.components.fraud_check_risk import FraudCheckRisk +from didit.types.components.fraud_next_action_enum import FraudNextActionEnum +from didit.types.components.fraud_processing_status_enum import FraudProcessingStatusEnum +from didit.types.components.fraud_recommendation_enum import FraudRecommendationEnum + +__all__ = ["FraudCheckResponse"] + + +class FraudCheckResponse(BaseModel): + check_id: str + + request_id: str | None = None + """Native verification session created for this check.""" + + client_reference: str | None + + processing_status: FraudProcessingStatusEnum + """* `pending` - Pending + * `running` - Running + * `completed` - Completed + * `failed` - Failed + """ + + profile_id: str + + profile_version: str + + created_at: _dt.datetime + + completed_at: _dt.datetime | None + + inputs: list[FraudCheckInput] + + modules: list[FraudCheckModuleReport] + + attribute_evidence: list[dict[str, object]] + + compound_evidence: list[dict[str, object]] + + risk: list[FraudCheckRisk] + + network: dict[str, object] + + enrichment: dict[str, object] | None = None + + identity_relationship: FraudCheckIdentityRelationship | None = None + + recommendation: FraudRecommendationEnum + """* `approve` - Approve + * `review` - Review + * `reject` - Reject + """ + + next_action: FraudNextActionEnum + """* `none` - None + * `manual_review` - Manual Review + * `step_up` - Step Up + * `resubmit_capture` - Resubmit Capture + """ + + decision_reasons: list[str] + + coverage: dict[str, object] + + usage: list[dict[str, object]] + + vendor_data: str | None = None + + metadata: object = None diff --git a/src/didit/types/components/fraud_check_risk.py b/src/didit/types/components/fraud_check_risk.py new file mode 100644 index 0000000..9d2a4a4 --- /dev/null +++ b/src/didit/types/components/fraud_check_risk.py @@ -0,0 +1,44 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.fraud_availability_enum import FraudAvailabilityEnum +from didit.types.components.fraud_band_enum import FraudBandEnum +from didit.types.components.fraud_dimension_enum import FraudDimensionEnum + +__all__ = ["FraudCheckRisk"] + + +class FraudCheckRisk(BaseModel): + dimension: FraudDimensionEnum + """* `identity` - Identity + * `contact` - Contact + * `device_network` - Device Network + * `behavioral` - Behavioral + * `fraud_network` - Fraud Network + """ + + score: int | None + + band: FraudBandEnum + """* `low` - Low + * `medium` - Medium + * `high` - High + * `unknown` - Unknown + """ + + availability: FraudAvailabilityEnum + """* `available` - Available + * `not_requested` - Not Requested + * `no_reference` - No Reference + * `insufficient_history` - Insufficient History + * `unsupported_region` - Unsupported Region + * `source_unavailable` - Source Unavailable + """ + + requested_but_unavailable: bool + """True when the request supplied everything this dimension needed and the platform still could not answer it. Such a check is never recommended for approval on the strength of the dimensions that did answer.""" + + reason_codes: list[str] + + evidence_indexes: list[int] diff --git a/src/didit/types/components/fraud_check_subject_request.py b/src/didit/types/components/fraud_check_subject_request.py new file mode 100644 index 0000000..ca842fe --- /dev/null +++ b/src/didit/types/components/fraud_check_subject_request.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.fraud_check_attributes_request import FraudCheckAttributesRequest +from didit.types.components.fraud_check_identifier_request import FraudCheckIdentifierRequest + +__all__ = ["FraudCheckSubjectRequest"] + + +class FraudCheckSubjectRequest(TypedDict, total=False): + selfie: str + """Base64 JPEG, PNG or WebP selfie, up to 6 MB. No reference image or URL.""" + + identifiers: Required[Sequence[FraudCheckIdentifierRequest]] + """One or more typed identifiers. One is enough.""" + + attributes: FraudCheckAttributesRequest + """Corroborating attributes. All optional; they widen module coverage.""" diff --git a/src/didit/types/components/fraud_dimension_enum.py b/src/didit/types/components/fraud_dimension_enum.py new file mode 100644 index 0000000..6f9d7e3 --- /dev/null +++ b/src/didit/types/components/fraud_dimension_enum.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudDimensionEnum"] + + +FraudDimensionEnum: TypeAlias = ( + Literal["identity", "contact", "device_network", "behavioral", "fraud_network"] | str +) +"""* `identity` - Identity +* `contact` - Contact +* `device_network` - Device Network +* `behavioral` - Behavioral +* `fraud_network` - Fraud Network +""" diff --git a/src/didit/types/components/fraud_exclude_enum.py b/src/didit/types/components/fraud_exclude_enum.py new file mode 100644 index 0000000..f2f1157 --- /dev/null +++ b/src/didit/types/components/fraud_exclude_enum.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudExcludeEnum"] + + +FraudExcludeEnum: TypeAlias = ( + Literal[ + "normalization", + "email_risk", + "email_social", + "phone_social", + "phone_risk", + "ip_risk", + "database_validation", + "networks_same_org", + "networks_cross_org", + "biometric_identity", + ] + | str +) +"""* `normalization` - Normalization +* `email_risk` - Email Risk +* `email_social` - Email Social +* `phone_social` - Phone Social +* `phone_risk` - Phone Risk +* `ip_risk` - Ip Risk +* `database_validation` - Database Validation +* `networks_same_org` - Networks Same Org +* `networks_cross_org` - Networks Cross Org +* `biometric_identity` - Biometric Identity +""" diff --git a/src/didit/types/components/fraud_next_action_enum.py b/src/didit/types/components/fraud_next_action_enum.py new file mode 100644 index 0000000..7222227 --- /dev/null +++ b/src/didit/types/components/fraud_next_action_enum.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudNextActionEnum"] + + +FraudNextActionEnum: TypeAlias = ( + Literal["none", "manual_review", "step_up", "resubmit_capture"] | str +) +"""* `none` - None +* `manual_review` - Manual Review +* `step_up` - Step Up +* `resubmit_capture` - Resubmit Capture +""" diff --git a/src/didit/types/components/fraud_processing_status_enum.py b/src/didit/types/components/fraud_processing_status_enum.py new file mode 100644 index 0000000..2620259 --- /dev/null +++ b/src/didit/types/components/fraud_processing_status_enum.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudProcessingStatusEnum"] + + +FraudProcessingStatusEnum: TypeAlias = Literal["pending", "running", "completed", "failed"] | str +"""* `pending` - Pending +* `running` - Running +* `completed` - Completed +* `failed` - Failed +""" diff --git a/src/didit/types/components/fraud_recommendation_enum.py b/src/didit/types/components/fraud_recommendation_enum.py new file mode 100644 index 0000000..6dbd67e --- /dev/null +++ b/src/didit/types/components/fraud_recommendation_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudRecommendationEnum"] + + +FraudRecommendationEnum: TypeAlias = Literal["approve", "review", "reject"] | str +"""* `approve` - Approve +* `review` - Review +* `reject` - Reject +""" diff --git a/src/didit/types/components/fraud_result_enum.py b/src/didit/types/components/fraud_result_enum.py new file mode 100644 index 0000000..ab4518c --- /dev/null +++ b/src/didit/types/components/fraud_result_enum.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudResultEnum"] + + +FraudResultEnum: TypeAlias = Literal["match", "mismatch", "inconclusive", "no_reference"] | str +"""* `match` - Match +* `mismatch` - Mismatch +* `inconclusive` - Inconclusive +* `no_reference` - No Reference +""" diff --git a/src/didit/types/components/fraud_status_enum.py b/src/didit/types/components/fraud_status_enum.py new file mode 100644 index 0000000..92a0af7 --- /dev/null +++ b/src/didit/types/components/fraud_status_enum.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudStatusEnum"] + + +FraudStatusEnum: TypeAlias = Literal["planned", "completed", "skipped", "failed"] | str +"""* `planned` - Planned +* `completed` - Completed +* `skipped` - Skipped +* `failed` - Failed +""" diff --git a/src/didit/types/components/fraud_type_enum.py b/src/didit/types/components/fraud_type_enum.py new file mode 100644 index 0000000..4e00a00 --- /dev/null +++ b/src/didit/types/components/fraud_type_enum.py @@ -0,0 +1,34 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["FraudTypeEnum"] + + +FraudTypeEnum: TypeAlias = ( + Literal[ + "email", + "phone", + "national_id", + "tax_number", + "passport_number", + "driving_licence_number", + "residence_permit_number", + "voter_id", + "external_id", + ] + | str +) +"""* `email` - Email +* `phone` - Phone +* `national_id` - National Id +* `tax_number` - Tax Number +* `passport_number` - Passport Number +* `driving_licence_number` - Driving Licence Number +* `residence_permit_number` - Residence Permit Number +* `voter_id` - Voter Id +* `external_id` - External Id +""" diff --git a/src/didit/types/components/ip_risk_response.py b/src/didit/types/components/ip_risk_response.py new file mode 100644 index 0000000..f52ad28 --- /dev/null +++ b/src/didit/types/components/ip_risk_response.py @@ -0,0 +1,99 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["IPRiskResponse", "IPRiskResponseIp"] + + +class IPRiskResponseIp(BaseModel): + """IP risk result. Individual enrichment fields can be `null` when the lookup returned no usable data.""" + + status: Literal["Approved", "In Review"] | str + """`In Review` when the resolved country conflicts with `claimed_country`, or when the address is associated with VPN, Tor, or data-center infrastructure. Otherwise `Approved`.""" + + ip_address: str | None = None + """The scored address. `null` when no enrichment source returned usable data.""" + + ip_country: str | None = None + """Resolved country name.""" + + ip_country_code: str | None = None + """Resolved country code. Compare countries through `country_mismatch` instead of comparing raw country strings yourself.""" + + ip_state: str | None = None + """Resolved state or region.""" + + ip_city: str | None = None + """Resolved city.""" + + latitude: float | None = None + """Approximate network geolocation, not a precise device location.""" + + longitude: float | None = None + """Approximate network geolocation, not a precise device location.""" + + isp: str | None = None + """Internet service provider serving the address.""" + + organization: str | None = None + """Organization owning the address block.""" + + is_vpn_or_tor: bool | None = None + """`true` when the address is associated with VPN or Tor infrastructure.""" + + is_data_center: bool | None = None + """`true` when the address belongs to a hosting or data-center network.""" + + time_zone: str | None = None + """Time zone of the resolved location.""" + + time_zone_offset: str | None = None + """UTC offset of the resolved location.""" + + asn_number: float | None = None + """Autonomous system number.""" + + asn_organization: str | None = None + """Organization operating the autonomous system.""" + + connection_type: str | None = None + """Connection classification (e.g., `cellular`, `hosting`).""" + + carrier: str | None = None + """Mobile carrier when the address is on a mobile network.""" + + proxy_type: str | None = None + """Proxy classification when the address is behind a proxy.""" + + claimed_country: str | None = None + """Echo of the `claimed_country` you sent.""" + + country_mismatch: bool | None = None + """`true` when the resolved country differs from `claimed_country`. `null` when no enrichment data or no claimed country is available.""" + + threat: dict[str, object] + """Free-form network-threat evidence from the enrichment source (e.g., `is_proxy`, `is_tor`). Empty object when no enrichment data is available. Treat as diagnostic detail, not a stable contract.""" + + +class IPRiskResponse(BaseModel): + request_id: str + """Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity.""" + + status: Literal["Approved", "In Review"] | str + """Decision for this API session: `In Review` on network or country-mismatch risk, otherwise `Approved`.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, `null` when you did not send one.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent, `null` when you did not send one.""" + + created_at: _dt.datetime + """Timestamp of this response.""" + + ip: IPRiskResponseIp | None = None + """IP risk result. Individual enrichment fields can be `null` when the lookup returned no usable data.""" diff --git a/src/didit/types/components/kyb_registry_address.py b/src/didit/types/components/kyb_registry_address.py new file mode 100644 index 0000000..d701e4b --- /dev/null +++ b/src/didit/types/components/kyb_registry_address.py @@ -0,0 +1,34 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["KYBRegistryAddress"] + + +class KYBRegistryAddress(BaseModel): + """An address the registry states, in the same shape for every country and registry source. Every key is always present; a part the registry does not publish is null.""" + + type: str | None + """Address type in lower case: `registered` for the registered office; other types as the registry names them (for example `postal`, `office`). `null` when the registry does not say what the address is.""" + + address: str + """The full address as one line. Always present on every address.""" + + street: str | None + """Street and number.""" + + city: str | None + """City or locality.""" + + postal_code: str | None + """Postal code.""" + + region: str | None + """Region, state or province.""" + + country_code: str | None + """ISO 3166-1 alpha-2 country code.""" + + description: str | None + """The registry's own label for the address, when it gives one.""" diff --git a/src/didit/types/components/kyb_registry_industry.py b/src/didit/types/components/kyb_registry_industry.py new file mode 100644 index 0000000..6c76033 --- /dev/null +++ b/src/didit/types/components/kyb_registry_industry.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["KYBRegistryIndustry"] + + +class KYBRegistryIndustry(BaseModel): + """An activity the registry classifies the company under, in the same shape for every country and registry source.""" + + code: str | None + """Activity code as the registry prints it (for example `73110`).""" + + description: str | None + """Activity label, when the registry publishes one.""" + + scheme: str | None + """Classification system, one name per system whichever source reported it: `UK SIC 2007`, `US SIC`, `NACE Rev. 2`, `NACE Rev. 2.1`, `NAF Rev. 2`, `CNAE`, `SBI`, `NOGA 2008`, `CAE Rev. 4`, `Canada SIC 1977`; other national systems keep the registry's own name. Null when the registry does not name the system.""" + + is_primary: bool + """True for the company's main activity. Exactly one entry is primary when the list is not empty.""" diff --git a/src/didit/types/components/kyb_registry_ownership.py b/src/didit/types/components/kyb_registry_ownership.py new file mode 100644 index 0000000..3620a0f --- /dev/null +++ b/src/didit/types/components/kyb_registry_ownership.py @@ -0,0 +1,50 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["KYBRegistryOwnership", "KYBRegistryOwnershipUnresolved"] + + +class KYBRegistryOwnershipUnresolved(BaseModel): + reason: str | None = None + + section: str | None = None + + +class KYBRegistryOwnership(BaseModel): + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + status: Literal["not_requested", "pending", "delivered", "unavailable"] | str | None + """`delivered` when the registry published ownership; `unavailable` when it did not (see `unavailable_reason`); `pending` while the registry is still fetching it; `not_requested` for the Basic tier.""" + + tier_requested: Literal["basic", "shareholders", "ubo"] | str | None + """The tier the workflow ordered.""" + + tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None + """The tier the registry delivered (and that is billed). A UBO request answered with direct shareholders only is `shareholders`.""" + + unavailable_reason: ( + Literal[ + "no_owners_on_record", + "no_natural_person_owner", + "ownership_section_unavailable", + "company_not_found", + "ambiguous_company_match", + "missing_registration_number", + "entity_type_not_covered", + "provider_unavailable", + "other", + ] + | str + | None + ) + """Why a requested ownership tier was not delivered. Null unless `status` is `unavailable` or `pending`.""" + + direct_ownership_complete: bool | None + """True when the direct shareholders' stated stakes add up to 100% (within one percentage point of rounding); false when they do not; null when ownership was not delivered or a stake is not stated.""" + + unresolved: list[KYBRegistryOwnershipUnresolved] + """Parts of the ownership chain the registry could not resolve, for example no group structure on record above the direct owners.""" diff --git a/src/didit/types/components/kyb_registry_shareholding.py b/src/didit/types/components/kyb_registry_shareholding.py new file mode 100644 index 0000000..f230c74 --- /dev/null +++ b/src/didit/types/components/kyb_registry_shareholding.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["KYBRegistryShareholding"] + + +class KYBRegistryShareholding(BaseModel): + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + share_class: str | None + """Share class or type as the registry names it.""" + + number_of_shares: float | None + """Number of shares held.""" + + currency: str | None + """ISO 4217 currency of the values below.""" + + nominal_value: float | None + """Nominal value per share.""" + + total_value: float | None + """Total value of the holding.""" + + percentage: float | None + """Exact stake in percent, to four decimal places.""" diff --git a/src/didit/types/components/login_response.py b/src/didit/types/components/login_response.py new file mode 100644 index 0000000..6ca29f3 --- /dev/null +++ b/src/didit/types/components/login_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["LoginResponse"] + + +class LoginResponse(BaseModel): + access_token: str | None = None + """RS256-signed JWT. Use as `Authorization: Bearer ` for Account Management endpoints.""" + + refresh_token: str | None = None + + expires_in: int | None = None + """Lifetime of the access token in seconds (default 86400).""" + + message: str | None = None diff --git a/src/didit/types/components/network_detail.py b/src/didit/types/components/network_detail.py new file mode 100644 index 0000000..ac60ef1 --- /dev/null +++ b/src/didit/types/components/network_detail.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit.types.components.network_list_item import NetworkListItem + +__all__ = ["NetworkDetail"] + + +class NetworkDetail(NetworkListItem): + stats: dict[str, object] | None = None + """Aggregate stats for the network (e.g. member totals).""" + + fraud_tags: list[dict[str, object]] | None = None + """Fraud-pattern tags surfaced on the network.""" + + risk_signals: dict[str, object] | None = None + """Risk-signal summaries: residential_proxy_reuse, email_intelligence, phone_intelligence.""" + + reviewed_at: _dt.datetime | None = None + + reviewed_by: str | None = None + + review_reason: str | None = None + + notes_count: int | None = None + """Number of private analyst notes in this investigation.""" diff --git a/src/didit/types/components/network_graph.py b/src/didit/types/components/network_graph.py new file mode 100644 index 0000000..36ad09a --- /dev/null +++ b/src/didit/types/components/network_graph.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_detail import NetworkDetail +from didit.types.components.network_graph_edge import NetworkGraphEdge +from didit.types.components.network_graph_node import NetworkGraphNode + +__all__ = ["NetworkGraph", "NetworkGraphMetrics"] + + +class NetworkGraphMetrics(BaseModel): + node_count: int | None = None + + edge_count: int | None = None + + hub_signal_count: int | None = None + + +class NetworkGraph(BaseModel): + network: NetworkDetail | None = None + + depth: int | None = None + """Effective depth applied, or null when unbounded (depth=all).""" + + truncated: bool | None = None + """True if the neighborhood was cut off by depth.""" + + nodes: list[NetworkGraphNode] | None = None + + edges: list[NetworkGraphEdge] | None = None + + metrics: NetworkGraphMetrics | None = None diff --git a/src/didit/types/components/network_graph_edge.py b/src/didit/types/components/network_graph_edge.py new file mode 100644 index 0000000..e528bbc --- /dev/null +++ b/src/didit/types/components/network_graph_edge.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["NetworkGraphEdge"] + + +class NetworkGraphEdge(BaseModel): + id: str | None = None + + source: str | None = None + """Subject node id.""" + + target: str | None = None + """Signal node id.""" + + subject_kind: Literal["vendor_user", "vendor_business", "transaction_party"] | str | None = None + + signal_type: ( + Literal["ip_address", "device", "face", "document_number", "phone", "email", "address"] + | str + | None + ) = None + + confidence: float | None = None + """1.0 for an exact match.""" + + observation_count: int | None = None + + first_seen_at: _dt.datetime | None = None + + last_seen_at: _dt.datetime | None = None diff --git a/src/didit/types/components/network_graph_node.py b/src/didit/types/components/network_graph_node.py new file mode 100644 index 0000000..cf6a458 --- /dev/null +++ b/src/didit/types/components/network_graph_node.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["NetworkGraphNode"] + + +class NetworkGraphNode(BaseModel): + """A subject node or a signal node, distinguished by kind. Subject nodes (kind: vendor_user | vendor_business | transaction_party) carry id, kind, subject, node_status (approved | in_review | declined | not_completed), and tags. Signal nodes (kind: signal) carry id, kind, uuid, signal_type, subject_count, coverage, is_hub, is_datacenter_ip, is_high_collision_device, node_status (signal_active | signal_guarded), tags, risk_evidence, first_seen_at, and last_seen_at.""" + + id: str | None = None + + kind: Literal["vendor_user", "vendor_business", "transaction_party", "signal"] | str | None = ( + None + ) diff --git a/src/didit/types/components/network_list_item.py b/src/didit/types/components/network_list_item.py new file mode 100644 index 0000000..7de789c --- /dev/null +++ b/src/didit/types/components/network_list_item.py @@ -0,0 +1,65 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_risk_display import NetworkRiskDisplay + +__all__ = ["NetworkListItem"] + + +class NetworkListItem(BaseModel): + uuid: str | None = None + + network_id: str | None = None + """Human-readable per-organization sequential id, e.g. N-0001.""" + + name: str | None = None + + status: Literal["active", "in_review", "resolved", "dismissed"] | str | None = None + """Network lifecycle status. Distinct from the per-applicant outcome_status inside a network (approved/in_review/declined/not_completed).""" + + risk: NetworkRiskDisplay | None = None + + pattern_types: ( + list[ + Literal[ + "exact_same_device", + "similar_device", + "same_ip_address", + "same_address", + "similar_selfie_backgrounds", + "similar_poa_documents", + "same_document_number", + "same_phone_number", + "same_email", + ] + | str + ] + | None + ) = None + """The clustering patterns detected in this network.""" + + signal_types: ( + list[ + Literal["ip_address", "device", "face", "document_number", "phone", "email", "address"] + | str + ] + | None + ) = None + """The distinct signal types present in this network.""" + + member_counts: dict[str, object] | None = None + """Counts of members by category (e.g. total, per outcome_status).""" + + member_status: dict[str, object] | None = None + """Breakdown of members by outcome_status.""" + + signal_coverage: dict[str, object] | None = None + """Per signal_type coverage of this network's members.""" + + first_activity_at: _dt.datetime | None = None + + last_activity_at: _dt.datetime | None = None diff --git a/src/didit/types/components/network_map.py b/src/didit/types/components/network_map.py new file mode 100644 index 0000000..dcef68c --- /dev/null +++ b/src/didit/types/components/network_map.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.network_detail import NetworkDetail + +__all__ = ["NetworkMap", "NetworkMapPoints"] + + +class NetworkMapPoints(BaseModel): + uuid: str | None = None + + country: str | None = None + + country_code: str | None = None + + city: str | None = None + + latitude: float | None = None + + longitude: float | None = None + + observed_at: _dt.datetime | None = None + + +class NetworkMap(BaseModel): + network: NetworkDetail | None = None + + convergence_count: int | None = None + """Number of distinct address/location points members converge on.""" + + countries: list[str] | None = None + + points: list[NetworkMapPoints] | None = None diff --git a/src/didit/types/components/network_member.py b/src/didit/types/components/network_member.py new file mode 100644 index 0000000..dfec5d1 --- /dev/null +++ b/src/didit/types/components/network_member.py @@ -0,0 +1,50 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_node_tag import NetworkNodeTag +from didit.types.components.network_risk_display import NetworkRiskDisplay + +__all__ = ["NetworkMember", "NetworkMemberProvenance"] + + +class NetworkMemberProvenance(BaseModel): + session_ids: list[str] | None = None + + business_session_ids: list[str] | None = None + + transaction_ids: list[str] | None = None + + +class NetworkMember(BaseModel): + subject: dict[str, object] | None = None + """The vendor_user, vendor_business, or transaction_party this member represents.""" + + outcome_status: str | None = None + """The subject's verification/decision outcome.""" + + node_status: ( + Literal[ + "approved", "in_review", "declined", "not_completed", "signal_active", "signal_guarded" + ] + | str + | None + ) = None + """For a member row this is always one of the subject-outcome values: approved, in_review, declined, or not_completed.""" + + tags: list[NetworkNodeTag] | None = None + + risk: NetworkRiskDisplay | None = None + + signal_coverage: dict[str, object] | None = None + + shared_infrastructure: dict[str, object] | None = None + + joined_at: _dt.datetime | None = None + + last_seen_at: _dt.datetime | None = None + + provenance: NetworkMemberProvenance | None = None diff --git a/src/didit/types/components/network_node_tag.py b/src/didit/types/components/network_node_tag.py new file mode 100644 index 0000000..ae9b278 --- /dev/null +++ b/src/didit/types/components/network_node_tag.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["NetworkNodeTag"] + + +class NetworkNodeTag(BaseModel): + id: str | None = None + + name: str | None = None + + color: str | None = None + """Null for guard tags, whose color token the renderer owns.""" + + source: ( + Literal["subject_tag", "session_tag", "transaction_tag", "signal_guard"] | str | None + ) = None + """Where the tag came from: a tag on the user/business, a tag on a session in this network, a tag on a transaction a party belongs to, or a signal guard condition (e.g. hub, datacenter IP).""" + + category: Literal["customer", "quality"] | str | None = None + """customer for customer-defined tags, quality for signal-quality guard tags. Null when not applicable.""" diff --git a/src/didit/types/components/network_note.py b/src/didit/types/components/network_note.py new file mode 100644 index 0000000..51c9d5b --- /dev/null +++ b/src/didit/types/components/network_note.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["NetworkNote"] + + +class NetworkNote(BaseModel): + uuid: str + + actor_email: str + """Authenticated author, assigned by the server.""" + + comment: str + + client_reference: str + + created_at: _dt.datetime diff --git a/src/didit/types/components/network_risk_display.py b/src/didit/types/components/network_risk_display.py new file mode 100644 index 0000000..7a8c1cc --- /dev/null +++ b/src/didit/types/components/network_risk_display.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["NetworkRiskDisplay"] + + +class NetworkRiskDisplay(BaseModel): + score: int | None = None + """Risk score, 0-100.""" + + band: Literal["low", "medium", "high"] | str | None = None + """Band the score renders as. high >= 70, medium >= 40, otherwise low.""" + + bar: float | None = None + """Normalized 0-1 value for rendering a risk bar.""" diff --git a/src/didit/types/components/network_signal.py b/src/didit/types/components/network_signal.py new file mode 100644 index 0000000..f3bf1df --- /dev/null +++ b/src/didit/types/components/network_signal.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.network_node_tag import NetworkNodeTag + +__all__ = ["NetworkSignal"] + + +class NetworkSignal(BaseModel): + uuid: str | None = None + + signal_type: ( + Literal["ip_address", "device", "face", "document_number", "phone", "email", "address"] + | str + | None + ) = None + + subject_count: int | None = None + + coverage: float | None = None + + is_hub: bool | None = None + """True when this signal links an unusually large number of subjects.""" + + is_datacenter_ip: bool | None = None + + is_high_collision_device: bool | None = None + + node_status: Literal["signal_active", "signal_guarded"] | str | None = None + + tags: list[NetworkNodeTag] | None = None + + risk_evidence: dict[str, object] | None = None + + first_seen_at: _dt.datetime | None = None + + last_seen_at: _dt.datetime | None = None diff --git a/src/didit/types/components/network_timeline_event.py b/src/didit/types/components/network_timeline_event.py new file mode 100644 index 0000000..f7191de --- /dev/null +++ b/src/didit/types/components/network_timeline_event.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["NetworkTimelineEvent"] + + +class NetworkTimelineEvent(BaseModel): + event_type: str | None = None + """e.g. network_detected, signal_observed, network_in_review, network_resolved, network_dismissed.""" + + occurred_at: _dt.datetime | None = None + + label: str | None = None + + metadata: dict[str, object] | None = None diff --git a/src/didit/types/components/organization.py b/src/didit/types/components/organization.py new file mode 100644 index 0000000..a0b60e7 --- /dev/null +++ b/src/didit/types/components/organization.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["Organization"] + + +class Organization(BaseModel): + uuid: str | None = None + """Use as `{org_id}` on the application endpoints.""" + + name: str | None = None + """Display name of the organization.""" + + contact_email: str | None = None + """Primary contact email for the organization.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/organization_summary.py b/src/didit/types/components/organization_summary.py new file mode 100644 index 0000000..1fed63f --- /dev/null +++ b/src/didit/types/components/organization_summary.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["OrganizationSummary"] + + +class OrganizationSummary(BaseModel): + """Minimal organization shape returned inline by `POST /programmatic/verify-email/`.""" + + uuid: str | None = None + + name: str | None = None diff --git a/src/didit/types/components/phone_intelligence.py b/src/didit/types/components/phone_intelligence.py new file mode 100644 index 0000000..e22ee0e --- /dev/null +++ b/src/didit/types/components/phone_intelligence.py @@ -0,0 +1,37 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["PhoneIntelligence"] + + +class PhoneIntelligence(BaseModel): + """Normalized phone-intelligence block. Present only when the intelligence add-on ran for this lookup; when present it is also exposed under the `enrichment` alias with identical values.""" + + score: float | None = None + """Normalized 0-100 risk score. Higher is riskier.""" + + phone_type_risk: str | None = None + """`Low`, `Medium` or `High`, derived from the score.""" + + phone_trust_index: float | None = None + """Inverse of the score, for dashboards that prefer higher is better.""" + + network_stability_index: float | None = None + """Stability of the line's network.""" + + is_disposable: bool | None = None + """`true` when the number looks disposable or virtual.""" + + is_ported: bool | None = None + """`true` when the number has moved carrier.""" + + is_recently_ported: bool | None = None + """`true` when the port happened recently.""" + + carrier_type: str | None = None + """Line classification reported by the lookup (e.g., `mobile`).""" + + reason_codes: list[str] | None = None + """Machine-readable codes explaining the score. Values can grow over time, so do not reject unknown codes.""" diff --git a/src/didit/types/components/phone_match_item.py b/src/didit/types/components/phone_match_item.py new file mode 100644 index 0000000..815e344 --- /dev/null +++ b/src/didit/types/components/phone_match_item.py @@ -0,0 +1,37 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["PhoneMatchItem"] + + +class PhoneMatchItem(BaseModel): + """Another session of your application that used the same phone number with a different `vendor_data`.""" + + session_id: str | None = None + """Session id of the other verification that used this phone number.""" + + session_number: int | None = None + """Human-friendly sequential number of that session.""" + + vendor_data: str | None = None + """`vendor_data` of that session.""" + + verification_date: str | None = None + """When that session was created (`YYYY-MM-DDTHH:MM:SSZ`).""" + + phone_number: str | None = None + """The matched phone number.""" + + status: str | None = None + """Status of that session (e.g., `Approved`, `Declined`, `In Progress`).""" + + is_blocklisted: bool | None = None + """Whether the phone number is on your application's blocklist.""" + + api_service: str | None = None + """For API-type sessions, the standalone API that created it; `null` for regular verification sessions.""" + + source: str | None = None + """Where the match was found. Usually `session`; a blocklist hit via an organization list entry uses `list_entry`.""" diff --git a/src/didit/types/components/phone_risk_response.py b/src/didit/types/components/phone_risk_response.py new file mode 100644 index 0000000..d38aae5 --- /dev/null +++ b/src/didit/types/components/phone_risk_response.py @@ -0,0 +1,102 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.phone_intelligence import PhoneIntelligence +from didit.types.components.phone_match_item import PhoneMatchItem +from didit.types.components.phone_social_footprint import PhoneSocialFootprint +from didit.types.components.risk_lifecycle_event import RiskLifecycleEvent +from didit.types.components.risk_warning import RiskWarning + +__all__ = ["PhoneRiskResponse", "PhoneRiskResponsePhone", "PhoneRiskResponsePhoneCarrier"] + + +class PhoneRiskResponsePhoneCarrier(BaseModel): + """Current network serving the number. `null` when the lookup could not resolve a carrier.""" + + name: str | None = None + """Carrier name. Empty string when the lookup could not identify it.""" + + type: str | None = None + """Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, or similar.""" + + +class PhoneRiskResponsePhone(BaseModel): + """Normalized phone report, the same block used by Phone Verification responses.""" + + status: Literal["Approved", "In Review", "Declined"] | str + """Decision for this phone number. Mirrors the top-level `status`.""" + + phone_number_prefix: str | None = None + """Country calling code with leading `+` (e.g., `+1`).""" + + phone_number: str | None = None + """National number without the country calling code.""" + + full_number: str | None = None + """Complete number in E.164 format.""" + + country_code: str | None = None + """ISO 3166-1 alpha-2 country of the number.""" + + country_name: str | None = None + """Full country name derived from `country_code`.""" + + carrier: PhoneRiskResponsePhoneCarrier | None = None + """Current network serving the number. `null` when the lookup could not resolve a carrier.""" + + is_disposable: bool | None = None + """`true` when the number belongs to a temporary or burner number service.""" + + is_virtual: bool | None = None + """`true` when the line type is virtual (`voip`, `isp`, or `vpn`).""" + + verification_method: str | None = None + """OTP channel of the underlying verification. `null` for risk lookups, which never send a code.""" + + verification_attempts: int | None = None + """OTP sends for this number. `0` for risk lookups.""" + + verified_at: _dt.datetime | None = None + """When the number was last verified by an OTP. `null` for risk lookups.""" + + warnings: list[RiskWarning] | None = None + """Risk warnings recorded for this request. Empty array when there are none.""" + + lifecycle: list[RiskLifecycleEvent] | None = None + """Chronological audit trail of the request. Empty array for risk lookups without send events.""" + + matches: list[PhoneMatchItem] | None = None + """Other sessions of your application where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries. Empty array when there are no matches.""" + + phone_intelligence: PhoneIntelligence | None = None + """Normalized phone-intelligence block. Present only when the intelligence add-on ran for this lookup; when present it is also exposed under the `enrichment` alias with identical values.""" + + enrichment: PhoneIntelligence | None = None + """Normalized phone-intelligence block. Present only when the intelligence add-on ran for this lookup; when present it is also exposed under the `enrichment` alias with identical values.""" + + phone_social: PhoneSocialFootprint | None = None + """Social footprint of the phone number: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the phone number was found on none of them - a valid, billable answer and a common synthetic-identity signal.""" + + +class PhoneRiskResponse(BaseModel): + request_id: str + """Id of the API session created for this request. Store it with `vendor_data` if you need to correlate the result with later webhooks or console activity.""" + + status: Literal["Approved", "In Review", "Declined"] | str + """Final decision for this API session. Normally `Approved`; the risk evaluation can produce `In Review` or `Declined`.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, `null` when you did not send one.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent, `null` when you did not send one.""" + + created_at: _dt.datetime + """Timestamp of this response.""" + + phone: PhoneRiskResponsePhone | None = None + """Normalized phone report, the same block used by Phone Verification responses.""" diff --git a/src/didit/types/components/phone_social_footprint.py b/src/didit/types/components/phone_social_footprint.py new file mode 100644 index 0000000..d470111 --- /dev/null +++ b/src/didit/types/components/phone_social_footprint.py @@ -0,0 +1,68 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.social_footprint_category_summary import SocialFootprintCategorySummary +from didit.types.components.social_footprint_platform import SocialFootprintPlatform +from didit.types.components.social_footprint_profile import SocialFootprintProfile + +__all__ = ["PhoneSocialFootprint"] + + +class PhoneSocialFootprint(BaseModel): + """Social footprint of the phone number: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the phone number was found on none of them - a valid, billable answer and a common synthetic-identity signal.""" + + profiles_registered: int | None = None + """Platforms where the phone number is confirmed registered.""" + + profiles_checked: int | None = None + """Platforms evaluated for registration.""" + + names_found: int | None = None + """Platforms that returned an associated name.""" + + photos_found: int | None = None + """Platforms that returned a profile photo.""" + + communication_breadth: int | None = None + """Communication platforms with detected presence.""" + + digital_footprint_diversity: int | None = None + """Distinct platform categories with detected presence.""" + + digital_sophistication_index: int | None = None + """Composite digital-maturity indicator, 0-10.""" + + engagement_depth_index: int | None = None + """Profile-richness indicator, 0-10.""" + + subscription_affordability_index: int | None = None + """Free vs paid platform mix, 0-10.""" + + free_platforms: int | None = None + """Registrations on free platforms.""" + + full_paid_platforms: int | None = None + """Registrations on fully paid platforms.""" + + partial_paid_platforms: int | None = None + """Registrations on freemium platforms.""" + + whatsapp_profile_completeness: ( + Literal["Basic", "Partial", "Complete", "Not Present"] | str | None + ) = None + """Completeness of the WhatsApp profile. Phone only; null for email.""" + + registered_platforms: list[SocialFootprintPlatform] | None = None + """Platforms where registration is confirmed.""" + + not_registered_platforms: list[SocialFootprintPlatform] | None = None + """Platforms checked with a negative result.""" + + categories: dict[str, SocialFootprintCategorySummary] | None = None + """Registered count and ratio per platform category.""" + + profiles: dict[str, SocialFootprintProfile] | None = None + """Per-platform profile details, keyed by platform, when the platform exposes them.""" diff --git a/src/didit/types/components/phone_verification_check_response.py b/src/didit/types/components/phone_verification_check_response.py new file mode 100644 index 0000000..fbea1d2 --- /dev/null +++ b/src/didit/types/components/phone_verification_check_response.py @@ -0,0 +1,167 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.phone_social_footprint import PhoneSocialFootprint + +__all__ = [ + "PhoneVerificationCheckResponse", + "PhoneVerificationCheckResponsePhone", + "PhoneVerificationCheckResponsePhoneCarrier", + "PhoneVerificationCheckResponsePhoneLifecycle", + "PhoneVerificationCheckResponsePhoneMatches", + "PhoneVerificationCheckResponsePhoneWarnings", +] + + +class PhoneVerificationCheckResponsePhoneCarrier(BaseModel): + """Current network serving the number, from the carrier lookup run at finalization.""" + + name: str | None = None + """Carrier name. Empty string when the lookup could not identify it.""" + + type: str | None = None + """Line type reported by the lookup: `mobile`, `fixed_line`, `voip`, `isp`, `vpn`, `toll_free`, `premium_rate`, `pager`, `payphone`, `satellite`, `service`, `shared_cost`, `short_codes_commercial`, `calling_cards`, `local_rate`, `universal_access`, `voice_mail`, `other`, or `unknown`.""" + + +class PhoneVerificationCheckResponsePhoneLifecycle(BaseModel): + type: str | None = None + """Event type.""" + + timestamp: _dt.datetime | None = None + """When the event happened.""" + + details: dict[str, object] | None = None + """Event-specific payload. Send events: `{status, reason, channel, actual_channel}` (`actual_channel` is filled once delivery is confirmed). Delivery events: `{channel, status}`. Code-entry events: `{code_tried, status}`. Final-status events: `null`, or `{reason}` when declined or in review.""" + + fee: float | None = None + """USD amount billed for this event. Non-zero only on send events (the delivery price); 0 for code entries, delivery confirmations, and status events.""" + + +class PhoneVerificationCheckResponsePhoneMatches(BaseModel): + session_id: str | None = None + """Session id of the other verification that used this phone number.""" + + session_number: int | None = None + """Human-friendly sequential number of that session.""" + + vendor_data: str | None = None + """`vendor_data` of that session.""" + + verification_date: str | None = None + """When that session was created (`YYYY-MM-DDTHH:MM:SSZ`).""" + + phone_number: str | None = None + """The matched phone number.""" + + status: str | None = None + """Status of that session (e.g., `Approved`, `Declined`, `In Progress`).""" + + is_blocklisted: bool | None = None + """Whether the phone number is on your application's blocklist.""" + + api_service: str | None = None + """For API-type sessions, the standalone API that created it (e.g., `PHONE_VERIFICATION`); `null` for regular verification sessions.""" + + source: str | None = None + """Where the match was found. Usually `session`; a blocklist hit via an organization list entry prepends a synthetic match with `source: "list_entry"` and null `session_id`/`session_number`.""" + + +class PhoneVerificationCheckResponsePhoneWarnings(BaseModel): + feature: str | None = None + """Feature that produced the warning. Always `PHONE` here.""" + + risk: str | None = None + """Machine-readable risk code.""" + + additional_data: dict[str, object] | None = None + """Risk-specific context (e.g., the duplicated session id for duplicate risks). `null` when the risk carries no extra data.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity. `error` risks decline the verification, `warning` flags it for review, `information` is recorded without affecting the status.""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class PhoneVerificationCheckResponsePhone(BaseModel): + """Full phone report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.""" + + status: Literal["Approved", "Declined", "In Review", "Not Finished", "Expired"] | str | None = ( + None + ) + """Final status of the phone verification. Matches the top-level `status` for finalized checks.""" + + phone_number_prefix: str | None = None + """Country calling code with leading `+` (e.g., `+1`).""" + + phone_number: str | None = None + """National number without the country calling code.""" + + full_number: str | None = None + """Complete number in E.164 format.""" + + country_code: str | None = None + """ISO 3166-1 alpha-2 country of the number.""" + + country_name: str | None = None + """Full country name derived from `country_code`.""" + + carrier: PhoneVerificationCheckResponsePhoneCarrier | None = None + """Current network serving the number, from the carrier lookup run at finalization.""" + + is_disposable: bool | None = None + """`true` when the number belongs to a temporary/burner number service.""" + + is_virtual: bool | None = None + """`true` when the line type is virtual (`voip`, `isp`, or `vpn`).""" + + verification_method: str | None = None + """Channel that actually delivered the OTP once delivery is confirmed (e.g., `whatsapp`, `sms`); until then, the requested channel.""" + + verification_attempts: int | None = None + """Number of OTP sends for this verification (1, or 2 after a retry).""" + + verified_at: _dt.datetime | None = None + """When the correct code was entered. `null` if the verification was never approved.""" + + warnings: list[PhoneVerificationCheckResponsePhoneWarnings] | None = None + """Risk warnings recorded during the verification (duplicate, disposable, VoIP, attempts exceeded, blocklist, etc.). Empty array when there are none.""" + + lifecycle: list[PhoneVerificationCheckResponsePhoneLifecycle] | None = None + """Chronological audit trail of the verification. Event `type` is one of `PHONE_VERIFICATION_MESSAGE_SENT`, `PHONE_VERIFICATION_RETRY_MESSAGE_SENT`, `PHONE_VERIFICATION_BLOCKED`, `PHONE_DELIVERY_DELIVERED`, `PHONE_DELIVERY_UNDELIVERABLE`, `VALID_CODE_ENTERED`, `INVALID_CODE_ENTERED`, `PHONE_VERIFICATION_APPROVED`, `PHONE_VERIFICATION_DECLINED`, `PHONE_VERIFICATION_IN_REVIEW`, `PHONE_VERIFICATION_EXPIRED`.""" + + matches: list[PhoneVerificationCheckResponsePhoneMatches] | None = None + """Other sessions of your application (KYC, KYB, or API) where the same phone number was used by a different user (different `vendor_data`). Up to 5 entries, a synthetic list-entry blocklist match first when present (session-derived matches keep query order). Empty array when there are no matches.""" + + phone_social: PhoneSocialFootprint | None = None + """Social footprint of the phone number: the online platforms it is registered on. Present only when `enable_social` was set and the check completed. Every count may be null when no data was returned for it. `profiles_registered: 0` means the platforms were checked and the phone number was found on none of them - a valid, billable answer and a common synthetic-identity signal.""" + + +class PhoneVerificationCheckResponse(BaseModel): + request_id: str | None = None + """On `Approved`/`Declined`: the session id of the matched verification - identical to the `request_id` returned by `POST /v3/phone/send/`. On `Failed` and `Expired or Not Found`: a random one-off UUID that cannot be looked up later.""" + + status: Literal["Approved", "Declined", "Failed", "Expired or Not Found"] | str | None = None + """`Approved` - correct code, no declining risk. `Declined` - terminal: a declining risk matched or the attempt budget (3) was exhausted. `Failed` - wrong code, attempts remaining. `Expired or Not Found` - no pending verification for this number in the last 5 minutes.""" + + message: str | None = None + """Human-readable explanation of the outcome, including the number of attempts remaining after a wrong code.""" + + phone: PhoneVerificationCheckResponsePhone | None = None + """Full phone report. Present (non-null) only on finalized outcomes (`Approved`/`Declined`); `null` on `Failed` and absent on `Expired or Not Found`.""" + + vendor_data: str | None = None + """`vendor_data` of the matched verification's session. `null` on `Expired or Not Found`.""" + + metadata: dict[str, object] | None = None + """`metadata` of the matched verification's session. `null` on `Expired or Not Found`.""" + + created_at: _dt.datetime | None = None + """Timestamp of this check response.""" diff --git a/src/didit/types/components/phone_verification_send_response.py b/src/didit/types/components/phone_verification_send_response.py new file mode 100644 index 0000000..ba91243 --- /dev/null +++ b/src/didit/types/components/phone_verification_send_response.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["PhoneVerificationSendResponse"] + + +class PhoneVerificationSendResponse(BaseModel): + request_id: str | None = None + """Session id of the verification. A `Retry` send returns the same `request_id` as the original send. This id appears in the Business Console, is returned again by a finalized `POST /v3/phone/check/`, and can be passed to `GET /v3/session/{sessionId}/decision/`.""" + + status: Literal["Success", "Retry", "Blocked"] | str | None = None + """`Success` - OTP sent to a new verification. `Retry` - OTP re-sent for the pending verification created by a previous send. `Blocked` - the anti-fraud layer refused to send; the verification is immediately finalized as `Declined` and nothing is billed.""" + + reason: str | None = None + """Why a send was `Blocked` (e.g., `repeated_attempts`, `suspicious`, `spam`). `null` on `Success` and `Retry`.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` stored on the session (from the first send).""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` stored on the session (from the first send).""" diff --git a/src/didit/types/components/questionnaire_detail.py b/src/didit/types/components/questionnaire_detail.py new file mode 100644 index 0000000..32469da --- /dev/null +++ b/src/didit/types/components/questionnaire_detail.py @@ -0,0 +1,46 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["QuestionnaireDetail"] + + +class QuestionnaireDetail(BaseModel): + """Full questionnaire detail returned from GET, POST (create) and PATCH (update) endpoints.""" + + questionnaire_id: str | None = None + """Unique identifier of the questionnaire. Use this id when referencing the questionnaire from a workflow or in subsequent update/delete requests.""" + + title: str | None = None + + description: str | None = None + + languages: list[str] | None = None + + default_language: str | None = None + + is_active: bool | None = None + + is_simple_questionnaire: bool | None = None + + graph: dict[str, object] | None = None + """Graph structure with start_node and nodes map.""" + + sections: list[dict[str, object]] | None = None + """Questionnaire content grouped into sections (derived from the graph).""" + + questionnaire_group_id: str | None = None + """Stable identifier that groups all versions of the same questionnaire.""" + + version: int | None = None + + status: Literal["draft", "published"] | str | None = None + + published_at: _dt.datetime | None = None + + is_editable: bool | None = None + """True when the questionnaire can still be edited in place (draft versions).""" diff --git a/src/didit/types/components/questionnaire_list_item.py b/src/didit/types/components/questionnaire_list_item.py new file mode 100644 index 0000000..865135b --- /dev/null +++ b/src/didit/types/components/questionnaire_list_item.py @@ -0,0 +1,53 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["QuestionnaireListItem"] + + +class QuestionnaireListItem(BaseModel): + """One questionnaire version in the list view.""" + + uuid: str | None = None + """Per-version UUID. Use as `{questionnaire_uuid}` for get/update/delete and as `questionnaire_uuid` in a workflow's QUESTIONNAIRE feature config.""" + + title: str | None = None + + description: str | None = None + + languages: list[str] | None = None + + default_language: str | None = None + + is_active: bool | None = None + + is_simple_questionnaire: bool | None = None + """True for linear questionnaires created through this API; false for graph-based questionnaires built in the Console.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + question_types: list[str] | None = None + """Uppercase element types of the answerable questions, in creation order (e.g. `SHORT_TEXT`, `MULTIPLE_CHOICE`, `FILE_UPLOAD`). Structural `SECTION_HEADER` and `SEPARATOR` elements are excluded. One entry per question, so values can repeat.""" + + workflow_names: list[str] | None = None + """Names of workflows using this questionnaire. Currently always `[]` on this endpoint (populated only in the Console listing).""" + + questionnaire_group_id: str | None = None + """Stable identifier that groups all versions of the same questionnaire.""" + + version: int | None = None + """Version number within the group, starting at 1.""" + + status: Literal["draft", "published"] | str | None = None + + published_at: _dt.datetime | None = None + """When this version was published. `null` for drafts.""" + + is_editable: bool | None = None + """True when this version is a `draft`.""" diff --git a/src/didit/types/components/register_response.py b/src/didit/types/components/register_response.py new file mode 100644 index 0000000..7ed2164 --- /dev/null +++ b/src/didit/types/components/register_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["RegisterResponse"] + + +class RegisterResponse(BaseModel): + message: str | None = None + """Human-readable confirmation that the verification code was sent.""" + + email: str | None = None + """The email the verification code was sent to.""" diff --git a/src/didit/types/components/regulatory_report_detail.py b/src/didit/types/components/regulatory_report_detail.py new file mode 100644 index 0000000..cdf39c2 --- /dev/null +++ b/src/didit/types/components/regulatory_report_detail.py @@ -0,0 +1,107 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "RegulatoryReportDetail", + "RegulatoryReportDetailNarrative", + "RegulatoryReportDetailValidationErrors", +] + + +class RegulatoryReportDetailNarrative(BaseModel): + """Five Ws narrative sections.""" + + who: str | None = None + + what: str | None = None + + when: str | None = None + + where: str | None = None + + why: str | None = None + + how: str | None = None + + +class RegulatoryReportDetailValidationErrors(BaseModel): + field: str | None = None + + message: str | None = None + + +class RegulatoryReportDetail(BaseModel): + uuid: str | None = None + + case_uuid: str | None = None + + case_number: str | None = None + + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str | None = None + + report_type_display: str | None = None + + report_code: str | None = None + """goAML report code derived from report_type, e.g. STR, CTR, BCR.""" + + report_number: str | None = None + + status: Literal["DRAFT", "VALIDATED", "FINALIZED"] | str | None = None + + status_display: str | None = None + + jurisdiction: str | None = None + + rentity_id: str | None = None + """Reporting entity ID issued by the FIU.""" + + rentity_branch: str | None = None + + submission_code: Literal["E", "M"] | str | None = None + + entity_reference: str | None = None + + fiu_ref_number: str | None = None + + currency_code_local: str | None = None + + submission_date: _dt.datetime | None = None + + reporting_person: dict[str, object] | None = None + + location: dict[str, object] | None = None + + reason: str | None = None + + action: str | None = None + + activity: str | None = None + + narrative: RegulatoryReportDetailNarrative | None = None + """Five Ws narrative sections.""" + + report_indicators: list[str] | None = None + + included_fields: list[str] | None = None + + transactions: list[str] | None = None + + validation_errors: list[RegulatoryReportDetailValidationErrors] | None = None + """Result of the most recent validation; each entry is a {field, message} object.""" + + validated_at: _dt.datetime | None = None + + finalized_at: _dt.datetime | None = None + + created_by_email: str | None = None + + created_by_name: str | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/report_template_detail.py b/src/didit/types/components/report_template_detail.py new file mode 100644 index 0000000..e23f3be --- /dev/null +++ b/src/didit/types/components/report_template_detail.py @@ -0,0 +1,29 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["ReportTemplateDetail"] + + +class ReportTemplateDetail(BaseModel): + uuid: str | None = None + + name: str | None = None + + jurisdiction: str | None = None + """ISO 3166-1 alpha-2 country code.""" + + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str | None = None + + report_type_display: str | None = None + + field_config: dict[str, object] | None = None + """Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/review_item.py b/src/didit/types/components/review_item.py new file mode 100644 index 0000000..a98352e --- /dev/null +++ b/src/didit/types/components/review_item.py @@ -0,0 +1,103 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["ReviewItem"] + + +class ReviewItem(BaseModel): + """One entry in a session's review / activity feed. Status changes carry `previous_status`, `new_status`, `previous_value`, `new_value`, and `changed_fields`; comments carry `comment` and (for Console comments) `mentioned_emails`; the remaining fields are `null` or empty when they do not apply to the activity type.""" + + uuid: str | None = None + """Unique identifier of this feed entry.""" + + activity_type: ( + Literal[ + "COMMENT", + "STATUS_UPDATED", + "KYC_DATA_UPDATED", + "POA_DATA_UPDATED", + "FILE_ADDED", + "FILE_REMOVED", + "AML_ONGOING_MATCH", + "AML_STATUS_UPDATED", + "AML_HIT_STATUS_UPDATED", + "KYB_DATA_UPDATED", + "KYB_DOCUMENT_UPLOADED", + "TAG_ADDED", + "TAG_REMOVED", + ] + | str + | None + ) = None + """What kind of activity this entry records. `STATUS_UPDATED` rows come from manual decisions (`PATCH /v3/session/{session_id}/update-status/`) and from entries added via `POST …/reviews/` (which default to this type even when no status is recorded). `COMMENT` rows come from Console reviewers; Console detail-page views are also stored as `COMMENT` rows flagged with `metadata.system_event = "DETAIL_VIEWED"`. The remaining types track data edits, file changes, tags, and AML monitoring activity. STATUS_UPDATED rows are also written by system actors (e.g. AML ongoing monitoring with actor_type SYSTEM, and automatic feature-status updates), not only by the update-status endpoint and manual review posts.""" + + actor_display: str | None = None + """Human-readable author label, resolved in order: actor's full name → Console account identifier → local part of `actor_email` → `"API Client"` (for `API_KEY` actors) → `"System"` (for `SYSTEM` actors) → `"Unknown"`. Entries created via `POST …/reviews/` always display `"Unknown"` because that endpoint records no actor attribution.""" + + actor_email: str | None = None + """Email of the Console user who performed the action. `null` for API-key and system actors.""" + + actor_type: Literal["API_KEY", "CONSOLE_USER", "SYSTEM"] | str | None = None + """Who performed the action: an API key, a Console user, or Didit itself (`SYSTEM`, e.g. AML ongoing monitoring). Entries created via `POST …/reviews/` carry the default `CONSOLE_USER` regardless of the caller.""" + + new_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Session status recorded by this entry (for status updates). `null` for comments and data-change entries.""" + + previous_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Status the session had before the change. Populated on `STATUS_UPDATED` entries written by the update-status endpoint; `null` on entries added via `POST …/reviews/`. Also populated on system-written STATUS_UPDATED rows.""" + + comment: str | None = None + """Free-text note attached to the entry, or `null`.""" + + mentioned_emails: list[str] | None = None + """Console users `@email`-mentioned in the comment. Always empty for entries created via this API - mention processing is a Console-only feature.""" + + previous_value: dict[str, object] | None = None + """Snapshot of the changed data before the action, e.g. `{"status": "In Review"}` on a status change. `null` when nothing was changed.""" + + new_value: dict[str, object] | None = None + """Snapshot of the changed data after the action, e.g. `{"status": "Approved"}`. `null` when nothing was changed.""" + + changed_fields: list[str] | None = None + """Names of the fields modified by this activity (e.g. `["status"]`). Empty for comments and for entries added via `POST …/reviews/`.""" + + metadata: dict[str, object] | None = None + """Extra context for system-generated entries. Console detail-page views carry `{"system_event": "DETAIL_VIEWED", "detail_type": "verification"}` so they can be filtered out of notification logic. `null` for ordinary entries.""" + + created_at: _dt.datetime | None = None + """When the entry was written, ISO 8601 UTC.""" diff --git a/src/didit/types/components/risk_lifecycle_event.py b/src/didit/types/components/risk_lifecycle_event.py new file mode 100644 index 0000000..fc23516 --- /dev/null +++ b/src/didit/types/components/risk_lifecycle_event.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["RiskLifecycleEvent"] + + +class RiskLifecycleEvent(BaseModel): + type: str + """Machine-readable event type. New values can be added over time.""" + + timestamp: _dt.datetime + """When the event happened.""" + + details: dict[str, object] | None + """Event-specific payload, or `null`.""" + + fee: float + """USD amount billed for this event.""" diff --git a/src/didit/types/components/risk_signals.py b/src/didit/types/components/risk_signals.py new file mode 100644 index 0000000..be89c9a --- /dev/null +++ b/src/didit/types/components/risk_signals.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["RiskSignals"] + + +class RiskSignals(TypedDict, total=False): + """Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks.""" + + ip: str + """IPv4 or IPv6 address observed for the end user.""" + + device_id: str + """Stable device identifier collected by your application (Android `ANDROID_ID`, iOS `identifierForVendor`).""" + + device_platform: Literal["android", "ios", "ipados", "tvos", "web"] | str + """Platform of the end user's device.""" + + device_model: str + """Device model reported by your application.""" + + os_version: str + """Operating-system version.""" + + app_version: str + """Version of your application.""" + + user_agent: str + """Browser or app user agent.""" diff --git a/src/didit/types/components/risk_warning.py b/src/didit/types/components/risk_warning.py new file mode 100644 index 0000000..0e0f455 --- /dev/null +++ b/src/didit/types/components/risk_warning.py @@ -0,0 +1,28 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["RiskWarning"] + + +class RiskWarning(BaseModel): + feature: str + """Feature that produced the warning.""" + + risk: str + """Machine-readable risk code. New values can be added over time.""" + + additional_data: dict[str, object] | None = None + """Risk-specific context, or `null`.""" + + log_type: Literal["error", "warning", "information"] | str + """Severity used by the decision rules.""" + + short_description: str + """Human-readable one-line summary.""" + + long_description: str + """Human-readable explanation.""" diff --git a/src/didit/types/components/routing_publication_case_routing_rule.py b/src/didit/types/components/routing_publication_case_routing_rule.py new file mode 100644 index 0000000..0052175 --- /dev/null +++ b/src/didit/types/components/routing_publication_case_routing_rule.py @@ -0,0 +1,41 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel +from didit.types.components.routing_publication_routing_condition import ( + RoutingPublicationRoutingCondition, +) + +__all__ = ["RoutingPublicationCaseRoutingRule"] + + +class RoutingPublicationCaseRoutingRule(BaseModel): + uuid: str + + name: str + + description: str | None = None + + priority: int | None = None + + conditions: list[RoutingPublicationRoutingCondition] + + required_skills: list[str] | None = None + + blueprint: str + + blueprint_name: str + + team: str | None = None + + team_name: str + + is_active: bool | None = None + + version: int + + created_at: _dt.datetime + + updated_at: _dt.datetime diff --git a/src/didit/types/components/routing_publication_field_enum.py b/src/didit/types/components/routing_publication_field_enum.py new file mode 100644 index 0000000..b83ac19 --- /dev/null +++ b/src/didit/types/components/routing_publication_field_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["RoutingPublicationFieldEnum"] + + +RoutingPublicationFieldEnum: TypeAlias = Literal["source", "priority", "tags"] | str +"""* `source` - source +* `priority` - priority +* `tags` - tags +""" diff --git a/src/didit/types/components/routing_publication_operator_enum.py b/src/didit/types/components/routing_publication_operator_enum.py new file mode 100644 index 0000000..8e6b308 --- /dev/null +++ b/src/didit/types/components/routing_publication_operator_enum.py @@ -0,0 +1,15 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["RoutingPublicationOperatorEnum"] + + +RoutingPublicationOperatorEnum: TypeAlias = Literal["equals", "in", "contains"] | str +"""* `equals` - equals +* `in` - in +* `contains` - contains +""" diff --git a/src/didit/types/components/routing_publication_paginated_policy_revision_list.py b/src/didit/types/components/routing_publication_paginated_policy_revision_list.py new file mode 100644 index 0000000..f74a9df --- /dev/null +++ b/src/didit/types/components/routing_publication_paginated_policy_revision_list.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.routing_publication_policy_revision import ( + RoutingPublicationPolicyRevision, +) + +__all__ = ["RoutingPublicationPaginatedPolicyRevisionList"] + + +class RoutingPublicationPaginatedPolicyRevisionList(BaseModel): + count: int + + next: str | None = None + + previous: str | None = None + + results: list[RoutingPublicationPolicyRevision] diff --git a/src/didit/types/components/routing_publication_policy_draft.py b/src/didit/types/components/routing_publication_policy_draft.py new file mode 100644 index 0000000..e83a3c2 --- /dev/null +++ b/src/didit/types/components/routing_publication_policy_draft.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["RoutingPublicationPolicyDraft"] + + +class RoutingPublicationPolicyDraft(BaseModel): + revision: str | None = None + + base_version: int + + changes: object = None + + actor_email: str | None = None + + updated_at: _dt.datetime diff --git a/src/didit/types/components/routing_publication_policy_draft_response.py b/src/didit/types/components/routing_publication_policy_draft_response.py new file mode 100644 index 0000000..9091d1d --- /dev/null +++ b/src/didit/types/components/routing_publication_policy_draft_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.routing_publication_policy_draft import RoutingPublicationPolicyDraft + +__all__ = ["RoutingPublicationPolicyDraftResponse"] + + +class RoutingPublicationPolicyDraftResponse(BaseModel): + draft: RoutingPublicationPolicyDraft | None diff --git a/src/didit/types/components/routing_publication_policy_revision.py b/src/didit/types/components/routing_publication_policy_revision.py new file mode 100644 index 0000000..9d1ef72 --- /dev/null +++ b/src/didit/types/components/routing_publication_policy_revision.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["RoutingPublicationPolicyRevision"] + + +class RoutingPublicationPolicyRevision(BaseModel): + version: int + + snapshot: object + + actor_email: str | None = None + + published_at: _dt.datetime diff --git a/src/didit/types/components/routing_publication_routing_condition.py b/src/didit/types/components/routing_publication_routing_condition.py new file mode 100644 index 0000000..6472f99 --- /dev/null +++ b/src/didit/types/components/routing_publication_routing_condition.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.routing_publication_field_enum import RoutingPublicationFieldEnum +from didit.types.components.routing_publication_operator_enum import RoutingPublicationOperatorEnum + +__all__ = ["RoutingPublicationRoutingCondition"] + + +class RoutingPublicationRoutingCondition(BaseModel): + field: RoutingPublicationFieldEnum + """* `source` - source + * `priority` - priority + * `tags` - tags + """ + + operator: RoutingPublicationOperatorEnum + """* `equals` - equals + * `in` - in + * `contains` - contains + """ + + value: object diff --git a/src/didit/types/components/session_import_error.py b/src/didit/types/components/session_import_error.py new file mode 100644 index 0000000..df017ea --- /dev/null +++ b/src/didit/types/components/session_import_error.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["SessionImportError"] + + +class SessionImportError(BaseModel): + uuid: str + + row_number: int + + external_id: str | None = None + + error: str + + raw_row: dict[str, object] + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/session_import_job.py b/src/didit/types/components/session_import_job.py new file mode 100644 index 0000000..85a59a7 --- /dev/null +++ b/src/didit/types/components/session_import_job.py @@ -0,0 +1,50 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionImportJob"] + + +class SessionImportJob(BaseModel): + uuid: str + """Import job id. Use this value to poll progress and fetch row errors.""" + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED", "PAUSED", "CANCELLED"] | str + + import_type: ( + Literal["user_verification", "business_verification", "status_rules", "transactions"] | str + ) + + source_format: Literal["csv", "ndjson"] | str + + source_filename: str + + total_rows: int + + processed_rows: int + + checkpoint_row: int | None = None + + created_count: int + + updated_count: int + + skipped_count: int + + failed_count: int + + total_media_bytes: int | None = None + + last_error: str | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + started_at: _dt.datetime | None = None + + completed_at: _dt.datetime | None = None diff --git a/src/didit/types/components/simple_questionnaire_element.py b/src/didit/types/components/simple_questionnaire_element.py new file mode 100644 index 0000000..fc09c4e --- /dev/null +++ b/src/didit/types/components/simple_questionnaire_element.py @@ -0,0 +1,89 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = [ + "SimpleQuestionnaireElement", + "SimpleQuestionnaireElementOptions", + "SimpleQuestionnaireElementValidation", +] + + +class SimpleQuestionnaireElementOptions(TypedDict, total=False): + value: str + + label: Required[Mapping[str, str]] + + +class SimpleQuestionnaireElementValidation(TypedDict, total=False): + """Optional format validation for `short_text`, `long_text`, or `number`. Enforced while the user fills the field (they cannot proceed until it passes) and re-checked by the API on submission.""" + + pattern: str + """Regular expression (ECMA-262 subset) the value must fully match. For `short_text` and `long_text`. Do not add ^/$ anchors; the whole value is matched.""" + + min_length: int + """Minimum number of characters. For `short_text` and `long_text`.""" + + max_length: int + """Maximum number of characters. For `short_text` and `long_text`.""" + + min: float + """Minimum numeric value. For `number`.""" + + max: float + """Maximum numeric value. For `number`.""" + + error_message: Mapping[str, str] + """Translated message shown when validation fails, keyed by language code.""" + + +class SimpleQuestionnaireElement(TypedDict, total=False): + id: str + """Optional stable question id. If omitted, the API generates `q1`, `q2`, etc. Use lowercase letters, numbers, and underscores for readability.""" + + element_type: Required[ + Literal[ + "short_text", + "long_text", + "dropdown", + "single_choice", + "multiple_choice", + "number", + "image", + "file_upload", + "time", + "email", + "address", + "phone", + "country", + "date_picker", + "consent", + "section_header", + "separator", + "heading", + "paragraph", + ] + | str + ] + """Question type. The API accepts these lowercase values and their uppercase equivalents.""" + + label: Required[Mapping[str, str]] + """Question label by language code, for example `{ "en": "Source of funds" }`.""" + + description: Mapping[str, str] | None + + placeholder: Mapping[str, str] | None + + is_required: bool + + options: Sequence[SimpleQuestionnaireElementOptions] + """Use for `dropdown`, `single_choice`, and `multiple_choice`. If `value` is omitted, the API generates one from the option label.""" + + max_files: int + """Use with `file_upload` or `image`.""" + + validation: SimpleQuestionnaireElementValidation | None + """Optional format validation for `short_text`, `long_text`, or `number`. Enforced while the user fills the field (they cannot proceed until it passes) and re-checked by the API on submission.""" diff --git a/src/didit/types/components/simple_workflow_feature.py b/src/didit/types/components/simple_workflow_feature.py new file mode 100644 index 0000000..a86e3e4 --- /dev/null +++ b/src/didit/types/components/simple_workflow_feature.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["SimpleWorkflowFeature"] + + +class SimpleWorkflowFeature(TypedDict, total=False): + feature: Required[ + Literal[ + "OCR", + "NFC", + "LIVENESS", + "FACE_MATCH", + "PROOF_OF_ADDRESS", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "DATABASE_VALIDATION", + "AML", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + ] + """Feature to run. Use the uppercase values shown here.""" + + config: Mapping[str, object] + """Feature-specific settings. For `QUESTIONNAIRE`, include `questionnaire_uuid` with the `questionnaire_id` returned by the questionnaire create endpoint. For `OCR`, `methods` selects which ID Verification methods each country may use (document capture, non-doc lookup against a government or other authoritative source, and digital identity wallets); omit it and every country is document capture only.""" + + label: str + """Optional internal label for this workflow node.""" diff --git a/src/didit/types/components/social_footprint_category_summary.py b/src/didit/types/components/social_footprint_category_summary.py new file mode 100644 index 0000000..0a35955 --- /dev/null +++ b/src/didit/types/components/social_footprint_category_summary.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["SocialFootprintCategorySummary"] + + +class SocialFootprintCategorySummary(BaseModel): + registered: int | None = None + """Platforms in this category where registration is confirmed.""" + + ratio: float | None = None + """Share of the category's checked platforms with a registration, 0.0-1.0.""" diff --git a/src/didit/types/components/social_footprint_platform.py b/src/didit/types/components/social_footprint_platform.py new file mode 100644 index 0000000..ba7ef40 --- /dev/null +++ b/src/didit/types/components/social_footprint_platform.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SocialFootprintPlatform"] + + +class SocialFootprintPlatform(BaseModel): + platform: str | None = None + """Platform identifier.""" + + category: str | None = None + """Platform category, e.g. MESSAGING, SOCIAL_MEDIA, ECOMMERCE, PROFESSIONAL, EMAIL_PROVIDER.""" + + scope: Literal["global", "local"] | str | None = None + """`global` for worldwide platforms, `local` for region-specific ones.""" diff --git a/src/didit/types/components/social_footprint_profile.py b/src/didit/types/components/social_footprint_profile.py new file mode 100644 index 0000000..d9e891b --- /dev/null +++ b/src/didit/types/components/social_footprint_profile.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SocialFootprintProfile"] + + +class SocialFootprintProfile(BaseModel): + name: str | None = None + """Profile name, when the platform exposes one.""" + + photo: str | None = None + """Profile photo URL, when the platform exposes one.""" + + about: str | None = None + """Profile about/bio text, when the platform exposes one.""" + + privacy_status: Literal["PUBLIC", "PRIVATE"] | str | None = None + """Profile visibility, when the platform exposes it.""" + + business_account: bool | None = None + """Whether the profile is a business account, when the platform exposes it.""" diff --git a/src/didit/types/components/transaction_detail.py b/src/didit/types/components/transaction_detail.py new file mode 100644 index 0000000..8205a93 --- /dev/null +++ b/src/didit/types/components/transaction_detail.py @@ -0,0 +1,518 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "TransactionDetail", + "TransactionDetailActionRequiredVerificationSession", + "TransactionDetailActionRequiredWalletOwnership", + "TransactionDetailActivities", + "TransactionDetailAlerts", + "TransactionDetailDocuments", + "TransactionDetailDocumentsFiles", + "TransactionDetailNetworkSnapshot", + "TransactionDetailParties", + "TransactionDetailPartiesDevice", + "TransactionDetailPartiesDeviceNetworkContext", + "TransactionDetailPaymentMethods", + "TransactionDetailProviderResults", + "TransactionDetailRemediation", + "TransactionDetailRuleRuns", + "TransactionDetailTags", + "TransactionDetailTravelRule", +] + + +class TransactionDetailActionRequiredVerificationSession(BaseModel): + type: Literal["verification_session"] | str + + url: str + """Hosted verification URL to open for the end user.""" + + session_id: str | None = None + """Verification session id.""" + + session_token: str | None = None + """Session token, used by the mobile SDKs to launch the flow natively.""" + + status: str | None = None + """Verification session status.""" + + +class TransactionDetailActionRequiredWalletOwnership(BaseModel): + type: Literal["wallet_ownership"] | str + + url: str + """Hosted wallet-ownership widget URL. Open it in a real browser surface - on mobile use SFSafariViewController or a Chrome Custom Tab, never an embedded webview.""" + + widget_session_id: str | None = None + """Wallet-ownership widget session id.""" + + expires_at: _dt.datetime | None = None + """When the widget session expires.""" + + +class TransactionDetailActivities(BaseModel): + uuid: str | None = None + + activity_type: str | None = None + """E.g. `TRANSACTION_CREATED`, `TRANSACTION_NOTE_ADDED`, `TRANSACTION_STATUS_CHANGED`.""" + + source: str | None = None + + status: str | None = None + + title: str | None = None + + description: str | None = None + + metadata: dict[str, object] | None = None + + occurred_at: _dt.datetime | None = None + + +class TransactionDetailAlerts(BaseModel): + uuid: str | None = None + + title: str | None = None + + description: str | None = None + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str | None = None + + status: ( + Literal[ + "OPEN", + "INVESTIGATING", + "AWAITING_USER", + "PENDING_SAR", + "SAR_FILED", + "RESOLVED", + "DISMISSED", + ] + | str + | None + ) = None + + source: Literal["RULE", "PROVIDER", "MANUAL"] | str | None = None + + metadata: dict[str, object] | None = None + + created_at: _dt.datetime | None = None + + due_at: _dt.datetime | None = None + + +class TransactionDetailDocumentsFiles(BaseModel): + uuid: str | None = None + + file_url: str | None = None + """Short-lived signed download URL; fetch it when needed rather than storing it.""" + + original_filename: str | None = None + + file_size: int | None = None + + +class TransactionDetailDocuments(BaseModel): + uuid: str | None = None + + label: str | None = None + + note: str | None = None + + uploaded_by: str | None = None + + created_at: _dt.datetime | None = None + + files: list[TransactionDetailDocumentsFiles] | None = None + + +class TransactionDetailNetworkSnapshot(BaseModel): + """Graph snapshot of related transactions/parties used by the rules engine. Either the `network_snapshot` submitted with the transaction or the graph Didit builds from a successful AML wallet screening (which replaces a submitted one). `null` when neither exists.""" + + uuid: str | None = None + + nodes: list[dict[str, object]] | None = None + + edges: list[dict[str, object]] | None = None + + metrics: dict[str, object] | None = None + + +class TransactionDetailPartiesDeviceNetworkContext(BaseModel): + """Network context with IP address and geo-location.""" + + ip_address: str | None = None + """Client IP address.""" + + ip_country_code: str | None = None + """Geo-resolved country of the IP (ISO 3166-1 alpha-3).""" + + latitude: float | None = None + """Geographic latitude of the IP.""" + + longitude: float | None = None + """Geographic longitude of the IP.""" + + +class TransactionDetailPartiesDevice(BaseModel): + """Normalized device context, including server-side IP enrichment under network_context (geolocation, VPN/data-center flags) when an IP was provided.""" + + fingerprint: str | None = None + """Unique device fingerprint.""" + + persistent_device_id: str | None = None + """Stable cross-session device identifier (v2 fingerprint schema).""" + + user_agent: str | None = None + """Browser or app user agent string.""" + + browser_family: str | None = None + """Browser family (e.g. Chrome, Firefox, Safari).""" + + browser_version: str | None = None + """Browser version string.""" + + platform: str | None = None + """Platform identifier (web, ios, android, macos, windows, linux).""" + + os_family: str | None = None + """Operating system family (e.g. iOS, Android, Windows).""" + + os_version: str | None = None + """Operating system version string.""" + + accept_language: str | None = None + """Accept-Language header value.""" + + session_id: str | None = None + """Session identifier (stored in metadata).""" + + session_age_ms: float | None = None + """Session age in milliseconds.""" + + network_context: TransactionDetailPartiesDeviceNetworkContext | None = None + """Network context with IP address and geo-location.""" + + +class TransactionDetailParties(BaseModel): + uuid: str | None = None + + role: Literal["APPLICANT", "REMITTER", "BENEFICIARY", "COUNTERPARTY"] | str | None = None + """Party role. The create endpoint assigns `APPLICANT` (the subject) and `COUNTERPARTY`.""" + + entity_type: Literal["individual", "company", "external", "unhostedWallet"] | str | None = None + + kind: Literal["USER", "BUSINESS", "EXTERNAL"] | str | None = None + """`USER`/`BUSINESS` parties link back to a Didit-owned entity via `vendor_data`. `EXTERNAL` parties have no Didit entity behind them.""" + + vendor_data: str | None = None + """Your internal user/business identifier.""" + + full_name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + country_code: str | None = None + """Country code from the submitted `address.country` (typically ISO 3166-1 alpha-3).""" + + dob: _dt.date | None = None + """Date of birth from the submitted `date_of_birth`.""" + + address: dict[str, object] | None = None + """Address object as submitted. `{}` when omitted.""" + + institution_info: dict[str, object] | None = None + """Institution details as submitted. `{}` when omitted.""" + + device: TransactionDetailPartiesDevice | None = None + """Normalized device context, including server-side IP enrichment under network_context (geolocation, VPN/data-center flags) when an IP was provided.""" + + external_party_snapshot: dict[str, object] | None = None + """Frozen copy of the party data for `EXTERNAL` parties. `null` while the party is linked to a Didit user or business.""" + + +class TransactionDetailPaymentMethods(BaseModel): + uuid: str | None = None + + payment_method_type: ( + Literal[ + "bank_card", "bank_account", "e_wallet", "crypto_wallet", "unhosted_wallet", "other" + ] + | str + | None + ) = None + + label: str | None = None + + fingerprint: str | None = None + """SHA-256 over `type:account_id:issuing_country` - stable identifier to correlate reuse of the same instrument.""" + + account_id: str | None = None + """Account identifier (IBAN, wallet address, card token, …).""" + + issuing_country: str | None = None + + owner_kind: Literal["USER", "BUSINESS", "EXTERNAL"] | str | None = None + + vendor_data: str | None = None + """Your identifier for the owning user/business.""" + + owner_name: str | None = None + + institution_info: dict[str, object] | None = None + + details: dict[str, object] | None = None + """Raw `payment_method` object as submitted.""" + + external_owner_snapshot: dict[str, object] | None = None + + role: Literal["SOURCE", "DESTINATION", "FUNDING", "BENEFICIARY"] | str | None = None + """How this method was used in the transaction. The create endpoint assigns `SOURCE`/`DESTINATION` from the direction (outbound: subject = SOURCE; inbound: subject = DESTINATION).""" + + +class TransactionDetailProviderResults(BaseModel): + uuid: str | None = None + + provider: str | None = None + + result_type: ( + Literal[ + "FIAT_MONITORING", + "WALLET_SCREENING", + "TRANSACTION_SCREENING", + "TRAVEL_RULE", + "NETWORK_GRAPH", + "CUSTOM", + ] + | str + | None + ) = None + + status: str | None = None + + severity: str | None = None + + score: int | None = None + + summary: str | None = None + + payload: dict[str, object] | None = None + """Raw provider response payload.""" + + created_at: _dt.datetime | None = None + + +class TransactionDetailRemediation(BaseModel): + """Remediation session offered to the user when re-verification is required. Superseded by `action_required`, which is the canonical block; kept for backward compatibility.""" + + session_id: str | None = None + + session_token: str | None = None + + url: str | None = None + + status: str | None = None + + +class TransactionDetailRuleRuns(BaseModel): + uuid: str | None = None + + rule: str | None = None + """UUID of the rule that ran.""" + + rule_title: str | None = None + + rule_description: str | None = None + + rule_category: str | None = None + + matched: bool | None = None + """Whether the rule's conditions matched this transaction. A run is recorded only for matched rules, so live evaluations always carry `true`.""" + + is_test: bool | None = None + """True when the rule ran in TEST mode (no effect on status/score).""" + + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str | None = None + + severity: str | None = None + + score_delta: int | None = None + """Score added by this rule's `add_score` actions.""" + + status_target: str | None = None + """Status set by a `change_status` action, if any.""" + + action_summary: str | None = None + """Summary of the actions the rule executed, joined with `; ` (e.g. `score +30; change_status:IN_REVIEW; add_tag:high_risk_country`). Parts: `score +N`, `change_status:`, `add_tag:`, `add_note`, `add_to_list`, `open_case`. `null` when the rule executed no action.""" + + metadata: dict[str, object] | None = None + """Evaluation detail: `scope_match`, `conditions[]` and `aggregations[]`. Each condition entry carries `field`, `operator`, `expected_value`, the `current_value` read from the transaction, `matched`, `group_index` and `group_logic`. Each aggregation entry (velocity / count / sum) carries `metric`, `field`, `operator`, `expected_value`, the `computed_value` the aggregate evaluated to, `window`, `filters`, `matched` and `sample_references`; deferred async aggregations have `computed_value: null` and `deferred: true`.""" + + created_at: _dt.datetime | None = None + + +class TransactionDetailTags(BaseModel): + uuid: str | None = None + + name: str | None = None + + color: str | None = None + + description: str | None = None + + source: Literal["didit", "custom"] | str | None = None + """Whether the tag is a Didit preset or a custom tag.""" + + +class TransactionDetailTravelRule(BaseModel): + """Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists.""" + + uuid: str | None = None + + status: str | None = None + + protocol: str | None = None + + required: bool | None = None + + obligations_count: int | None = None + + originator_data: dict[str, object] | None = None + + beneficiary_data: dict[str, object] | None = None + + metadata: dict[str, object] | None = None + + +class TransactionDetail(BaseModel): + """Full monitoring record for one transaction, as returned by `GET /v3/transactions/{transaction_id}/` and `POST /v3/transactions/`.""" + + uuid: str | None = None + """Didit-stable transaction identifier. Use as `{transaction_id}` for follow-up calls.""" + + environment: Literal["live", "sandbox"] | str | None = None + """Environment of the application that owns the transaction. A `sandbox` transaction is stored but never evaluated, screened, notified or billed, so its empty `rule_runs` means "not evaluated".""" + + transaction_number: int | None = None + """Application-scoped sequential transaction number shown in Console (e.g. `4123`).""" + + txn_id: str | None = None + """The `transaction_id` you supplied at create time (max 128 chars). Unique per application.""" + + txn_date: _dt.datetime | None = None + """When the transaction occurred (from `transaction_at`; defaults to submission time).""" + + zone_id: str | None = None + """IANA time zone identifier provided at create time.""" + + transaction_type: str | None = None + """Top-level category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange`, `auditTrailEvent`. Note multi-word values are echoed back in camelCase even when submitted in snake_case.""" + + action_type: str | None = None + """Sub-type within the category (e.g. `deposit`, `withdrawal`, `transfer`). Defaults to the category when not supplied.""" + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + """Direction relative to the subject. Stored uppercase regardless of the casing submitted (`in`/`out`/`inbound`/`outbound` are accepted on input).""" + + status: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str | None = None + """Current monitoring verdict. Transactions are created `APPROVED`; rules may flip them to `IN_REVIEW`/`DECLINED` synchronously.""" + + source: Literal["LIVE", "IMPORTED"] | str | None = None + """How the record originated: `LIVE` for transactions submitted through the API or SDK, `IMPORTED` for transactions loaded through the console import.""" + + amount: str | None = None + """Transaction amount as a decimal string with trailing zeros stripped (e.g. `"1500"`, `"0.5"`, `"0.123456789012345678"`). Up to 18 decimal places.""" + + currency: str | None = None + """Currency code of `amount`. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`.""" + + currency_type: str | None = None + """`fiat` or `crypto`, as submitted in `currency_kind`.""" + + amount_in_default_currency: str | None = None + """Pre-converted amount you supplied, as a decimal string with trailing zeros stripped.""" + + default_currency_code: str | None = None + + preferred_currency_amount: str | None = None + """`amount` converted to the application's preferred currency, when available.""" + + preferred_currency_code: str | None = None + + payment_details: str | None = None + """Free-text payment reference or memo from the submission.""" + + payment_txn_id: str | None = None + """External payment system reference (e.g. blockchain transaction hash) from `payment_reference_id`.""" + + score: int | None = None + """Risk score accumulated by rules (typically 0–100). Higher = riskier.""" + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str | None = None + """Categorical risk severity set by rules/providers (`UNKNOWN`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). `null` until something sets it.""" + + decision_reason_code: str | None = None + """Machine-readable reason for the current `status`.""" + + decision_reason_label: str | None = None + """Human-readable label for `decision_reason_code`.""" + + vendor_data: str | None = None + """Convenience copy of the applicant's `vendor_data`.""" + + metadata: dict[str, object] | None = None + """Snapshot of the `subject` and `counterparty` payloads as submitted at create time.""" + + props: dict[str, object] | None = None + """The `custom_properties` you supplied at create time. Each key is addressable in rule conditions as `custom_values.`.""" + + tags: list[TransactionDetailTags] | None = None + """Tags attached to the transaction (manually or by rules).""" + + parties: list[TransactionDetailParties] | None = None + """Transaction parties (applicant, counterparty).""" + + payment_methods: list[TransactionDetailPaymentMethods] | None = None + """Payment methods linked to the transaction.""" + + documents: list[TransactionDetailDocuments] | None = None + """Files attached to the transaction from the console. `[]` when none.""" + + activities: list[TransactionDetailActivities] | None = None + """Activity log entries (creation, notes, manual reviews, status changes).""" + + alerts: list[TransactionDetailAlerts] | None = None + """Alerts raised by rules and providers.""" + + rule_runs: list[TransactionDetailRuleRuns] | None = None + """Per-rule execution results - which rule fired, with what score impact.""" + + provider_results: list[TransactionDetailProviderResults] | None = None + """Raw provider payloads (AML screening, blockchain analytics, etc.).""" + + travel_rule: TransactionDetailTravelRule | None = None + """Travel Rule transfer or check. Populated when `travel_rule_details` was submitted, and also when Travel Rule is enabled and the transaction was created with `transaction_category: travelRule` (Didit initiates the exchange itself, even without `travel_rule_details`). `null` when no transfer or check exists.""" + + network_snapshot: TransactionDetailNetworkSnapshot | None = None + """Graph snapshot of related transactions/parties used by the rules engine. Either the `network_snapshot` submitted with the transaction or the graph Didit builds from a successful AML wallet screening (which replaces a submitted one). `null` when neither exists.""" + + remediation: TransactionDetailRemediation | None = None + """Remediation session offered to the user when re-verification is required. Superseded by `action_required`, which is the canonical block; kept for backward compatibility.""" + + action_required: ( + TransactionDetailActionRequiredVerificationSession + | TransactionDetailActionRequiredWalletOwnership + | None + ) = None + """Pending end-user action required to complete this transaction, or null when there is none. The Didit SDKs auto-launch the action by default and refresh the transaction when it finishes. Two variants, discriminated by `type`: `verification_session` (a hosted verification session created by a rule action) and `wallet_ownership` (a wallet-ownership widget session auto-minted for a Travel Rule transfer that needs proof of wallet control, gated by the `auto_wallet_verification` Travel Rule setting). When both could apply, `wallet_ownership` takes precedence because it blocks the Travel Rule exchange.""" + + cost_breakdown: dict[str, object] | None = None + """Cost of this transaction: `total_price` and `items`, one per charge, each with `usage_type`, `price`, `details` and `created_at`. A re-screen of the same transaction is a new item.""" diff --git a/src/didit/types/components/transaction_rule_action.py b/src/didit/types/components/transaction_rule_action.py new file mode 100644 index 0000000..cf290a7 --- /dev/null +++ b/src/didit/types/components/transaction_rule_action.py @@ -0,0 +1,185 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypeAlias, TypedDict + +from didit._models import BaseModel + +__all__ = [ + "TransactionRuleAction", + "TransactionRuleActionParam", + "TransactionRuleActionVariant1", + "TransactionRuleActionVariant1Param", + "TransactionRuleActionVariant2", + "TransactionRuleActionVariant2Param", + "TransactionRuleActionVariant3", + "TransactionRuleActionVariant3Param", + "TransactionRuleActionVariant4", + "TransactionRuleActionVariant4Param", + "TransactionRuleActionVariant5", + "TransactionRuleActionVariant5Param", + "TransactionRuleActionVariant6", + "TransactionRuleActionVariant6Param", +] + + +class TransactionRuleActionVariant1(BaseModel): + """Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds.""" + + type: Literal["add_score"] | str + + value: int + """Points added to the transaction's risk score. May be negative.""" + + +class TransactionRuleActionVariant2(BaseModel): + """Directly set the transaction status. DECLINED always wins across matched rules.""" + + type: Literal["change_status"] | str + + value: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str + """Status to set immediately, regardless of score. Alias key: status.""" + + workflow_id: str | None = None + """REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires.""" + + +class TransactionRuleActionVariant3(BaseModel): + """Attach an application tag to the matched transaction.""" + + type: Literal["add_tags"] | str + + tag_uuid: str | None = None + """Existing application tag to attach.""" + + tag_name: str | None = None + """Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name.""" + + tag_color: str | None = None + """Hex color used when tag_name creates a new tag, e.g. #007AFE.""" + + +class TransactionRuleActionVariant4(BaseModel): + """Attach an automated analyst note to the matched transaction.""" + + type: Literal["add_note"] | str + + note: str + """Note text attached to the transaction (author: Rule Engine). Alias key: value.""" + + +class TransactionRuleActionVariant5(BaseModel): + """Add the subject's vendor_data to a blocklist, allowlist or custom list.""" + + type: Literal["add_to_list"] | str + + list_id: str + """List to add the subject's vendor_data to.""" + + +class TransactionRuleActionVariant6(BaseModel): + """Open (or add to) an investigation case. Only the first open_case action on a rule is applied.""" + + type: Literal["open_case"] | str + + blueprint: str | None = None + """Case blueprint UUID; defaults to the application's default blueprint.""" + + grouping: Literal["by_applicant", "by_rule_and_applicant"] | str | None = None + """Whether repeat matches join one case per applicant or one per rule+applicant.""" + + attach_matched_transaction: bool | None = None + """Attach the matched transaction to the case.""" + + +TransactionRuleAction: TypeAlias = ( + TransactionRuleActionVariant1 + | TransactionRuleActionVariant2 + | TransactionRuleActionVariant3 + | TransactionRuleActionVariant4 + | TransactionRuleActionVariant5 + | TransactionRuleActionVariant6 +) +"""One rule action. A rule can carry several; all are applied together when it matches. Rules in TEST mode record what they would have done without applying any action.""" + + +class TransactionRuleActionVariant1Param(TypedDict, total=False): + """Add points to the transaction's cumulative risk score; the total decides the status via the application's review/decline thresholds.""" + + type: Required[Literal["add_score"] | str] + + value: Required[int] + """Points added to the transaction's risk score. May be negative.""" + + +class TransactionRuleActionVariant2Param(TypedDict, total=False): + """Directly set the transaction status. DECLINED always wins across matched rules.""" + + type: Required[Literal["change_status"] | str] + + value: Required[Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str] + """Status to set immediately, regardless of score. Alias key: status.""" + + workflow_id: str + """REQUIRED when value is AWAITING_USER: the verification workflow the subject is asked to complete. Alias key: remediation_workflow_id. Didit creates a hosted remediation session for the subject when the rule fires.""" + + +class TransactionRuleActionVariant3Param(TypedDict, total=False): + """Attach an application tag to the matched transaction.""" + + type: Required[Literal["add_tags"] | str] + + tag_uuid: str + """Existing application tag to attach.""" + + tag_name: str + """Tag name to attach; created on first use (stored uppercased). Provide tag_uuid or tag_name.""" + + tag_color: str + """Hex color used when tag_name creates a new tag, e.g. #007AFE.""" + + +class TransactionRuleActionVariant4Param(TypedDict, total=False): + """Attach an automated analyst note to the matched transaction.""" + + type: Required[Literal["add_note"] | str] + + note: Required[str] + """Note text attached to the transaction (author: Rule Engine). Alias key: value.""" + + +class TransactionRuleActionVariant5Param(TypedDict, total=False): + """Add the subject's vendor_data to a blocklist, allowlist or custom list.""" + + type: Required[Literal["add_to_list"] | str] + + list_id: Required[str] + """List to add the subject's vendor_data to.""" + + +class TransactionRuleActionVariant6Param(TypedDict, total=False): + """Open (or add to) an investigation case. Only the first open_case action on a rule is applied.""" + + type: Required[Literal["open_case"] | str] + + blueprint: str + """Case blueprint UUID; defaults to the application's default blueprint.""" + + grouping: Literal["by_applicant", "by_rule_and_applicant"] | str + """Whether repeat matches join one case per applicant or one per rule+applicant.""" + + attach_matched_transaction: bool + """Attach the matched transaction to the case.""" + + +TransactionRuleActionParam: TypeAlias = ( + TransactionRuleActionVariant1Param + | TransactionRuleActionVariant2Param + | TransactionRuleActionVariant3Param + | TransactionRuleActionVariant4Param + | TransactionRuleActionVariant5Param + | TransactionRuleActionVariant6Param +) +"""One rule action. A rule can carry several; all are applied together when it matches. Rules in TEST mode record what they would have done without applying any action.""" diff --git a/src/didit/types/components/transaction_rule_aggregation.py b/src/didit/types/components/transaction_rule_aggregation.py new file mode 100644 index 0000000..188ceff --- /dev/null +++ b/src/didit/types/components/transaction_rule_aggregation.py @@ -0,0 +1,54 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleAggregation", "TransactionRuleAggregationParam"] + + +class TransactionRuleAggregation(BaseModel): + """Velocity check over the application's historical transactions inside the window. Every aggregation entry must match for the rule to match, regardless of evaluation_mode. An aggregation constrains only the window; the transaction being evaluated is constrained by scope and conditions, so repeat direction, currency or transaction_type on both halves when the rule promises them.""" + + metric: Literal["count", "sum", "max", "min", "avg", "distinct_count"] | str | None = None + """How the matching historical transactions are aggregated. unique_count is accepted as an alias of distinct_count.""" + + field: str | None = None + """Field aggregated by sum/max/min/avg/distinct_count. Ignored for count.""" + + operator: Literal["eq", "ne", "gt", "gte", "lt", "lte"] | str | None = None + """Comparison applied to the computed metric.""" + + value: float + """Threshold the computed metric is compared against.""" + + window: str | None = None + """Look-back window ending at the transaction's txn_date: m (MINUTES, not months), h (hours) or d (days), e.g. 30m, 24h, 7d, 30d.""" + + filters: dict[str, object] | None = None + """Narrows which historical transactions are aggregated. Keys are field paths from the condition field catalog; a value of "__current__" resolves to the same field's value on the transaction being evaluated (e.g. {"subject_vendor_data": "__current__"} scopes the window to the same subject), and a list value means membership. Filters compare by equality or membership ONLY - there is no lt/gte filter, so a range such as "historical transactions under 10,000" cannot be expressed here. Put the range on the metric instead (metric max with operator lt keeps every transaction in the window below the threshold).""" + + +class TransactionRuleAggregationParam(TypedDict, total=False): + """Velocity check over the application's historical transactions inside the window. Every aggregation entry must match for the rule to match, regardless of evaluation_mode. An aggregation constrains only the window; the transaction being evaluated is constrained by scope and conditions, so repeat direction, currency or transaction_type on both halves when the rule promises them.""" + + metric: Literal["count", "sum", "max", "min", "avg", "distinct_count"] | str + """How the matching historical transactions are aggregated. unique_count is accepted as an alias of distinct_count.""" + + field: str + """Field aggregated by sum/max/min/avg/distinct_count. Ignored for count.""" + + operator: Literal["eq", "ne", "gt", "gte", "lt", "lte"] | str + """Comparison applied to the computed metric.""" + + value: Required[float] + """Threshold the computed metric is compared against.""" + + window: str + """Look-back window ending at the transaction's txn_date: m (MINUTES, not months), h (hours) or d (days), e.g. 30m, 24h, 7d, 30d.""" + + filters: Mapping[str, object] + """Narrows which historical transactions are aggregated. Keys are field paths from the condition field catalog; a value of "__current__" resolves to the same field's value on the transaction being evaluated (e.g. {"subject_vendor_data": "__current__"} scopes the window to the same subject), and a list value means membership. Filters compare by equality or membership ONLY - there is no lt/gte filter, so a range such as "historical transactions under 10,000" cannot be expressed here. Put the range on the metric instead (metric max with operator lt keeps every transaction in the window below the threshold).""" diff --git a/src/didit/types/components/transaction_rule_backtest_result.py b/src/didit/types/components/transaction_rule_backtest_result.py new file mode 100644 index 0000000..c81c931 --- /dev/null +++ b/src/didit/types/components/transaction_rule_backtest_result.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["TransactionRuleBacktestResult"] + + +class TransactionRuleBacktestResult(BaseModel): + evaluated: int | None = None + """Transactions evaluated (most recent first, capped).""" + + matched: int | None = None + """Transactions the configuration would have matched.""" + + affected_entities: int | None = None + """Distinct subjects (by vendor_data) among the matches.""" + + period_days: int | None = None diff --git a/src/didit/types/components/transaction_rule_condition.py b/src/didit/types/components/transaction_rule_condition.py new file mode 100644 index 0000000..fdfa56d --- /dev/null +++ b/src/didit/types/components/transaction_rule_condition.py @@ -0,0 +1,112 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleCondition", "TransactionRuleConditionParam"] + + +class TransactionRuleCondition(BaseModel): + """A condition reads only the transaction being evaluated - it never filters the aggregation window.""" + + field: str + """Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison.""" + + operator: ( + Literal[ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "in", + "not_in", + "contains", + "not_contains", + "contains_any", + "regex", + "fuzzy_match", + "exists", + "is_not_empty", + "is_not_null", + "not_exists", + "is_empty", + "is_null", + ] + | str + ) + """Comparison operator. in/not_in test membership (value may be a list or a single scalar). contains/not_contains are case-insensitive substring checks against a string value. contains_any takes a list of strings and matches when any is a case-insensitive substring of the field value. regex matches the value pattern against the field. fuzzy_match compares against the value string with the required `score` threshold (0-100). gt/gte/lt/lte never match when either side is missing. exists/is_not_empty/is_not_null (aliases) and not_exists/is_empty/is_null (aliases) are presence checks that ignore `value`.""" + + value: object = None + """Comparison value. Its shape depends on the operator and value_type; omit for the presence-check operators.""" + + value_type: Literal["list", "field", "relative_date"] | str | None = None + """Optional value interpretation. `list`: value is a List UUID and the list's entries become the comparison value. `field`: value is another field path, compared field-to-field. `relative_date`: value is a relative-date object, e.g. {"direction": "past", "unit": "days", "amount": 30}.""" + + score: float | None = None + """Required when operator is fuzzy_match: the 0-100 similarity threshold.""" + + group_index: int | None = None + """Optional grouped logic. When any condition carries group_index, conditions sharing an index form a group and grouped evaluation replaces evaluation_mode.""" + + group_logic: Literal["AND", "OR"] | str | None = None + """How conditions combine WITHIN a group.""" + + groups_logic: Literal["AND", "OR"] | str | None = None + """How group outcomes combine ACROSS groups. Read from the first condition.""" + + +class TransactionRuleConditionParam(TypedDict, total=False): + """A condition reads only the transaction being evaluated - it never filters the aggregation window.""" + + field: Required[str] + """Transaction field path to evaluate. Core fields: amount, currency, direction, action_type, score, severity, transaction_type, payment_details, preferred_currency_amount (falls back to amount_in_default_currency), default_currency_code, subject_country, counterparty_country, subject_vendor_data, counterparty_vendor_data, subject_device_fingerprint, counterparty_device_fingerprint, subject_browser_family, subject_browser_version, subject_platform, subject_accept_language, subject_session_age_ms, subject_ip_country, counterparty_ip_country, subject_ip_address, counterparty_ip_address, subject_payment_method_type, subject_payment_method_country, subject_payment_method_fingerprint, subject_payment_method_account_id (and the counterparty_payment_method_* equivalents), travel_rule_status, travel_rule_required, travel_rule_obligations_count, subject_days_since_previous_transaction, tags, and custom_values. for any key submitted in custom_properties on transaction create. Country-code fields accept ISO-2 or ISO-3; both sides are normalized to ISO-3 before comparison.""" + + operator: Required[ + Literal[ + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "in", + "not_in", + "contains", + "not_contains", + "contains_any", + "regex", + "fuzzy_match", + "exists", + "is_not_empty", + "is_not_null", + "not_exists", + "is_empty", + "is_null", + ] + | str + ] + """Comparison operator. in/not_in test membership (value may be a list or a single scalar). contains/not_contains are case-insensitive substring checks against a string value. contains_any takes a list of strings and matches when any is a case-insensitive substring of the field value. regex matches the value pattern against the field. fuzzy_match compares against the value string with the required `score` threshold (0-100). gt/gte/lt/lte never match when either side is missing. exists/is_not_empty/is_not_null (aliases) and not_exists/is_empty/is_null (aliases) are presence checks that ignore `value`.""" + + value: object + """Comparison value. Its shape depends on the operator and value_type; omit for the presence-check operators.""" + + value_type: Literal["list", "field", "relative_date"] | str + """Optional value interpretation. `list`: value is a List UUID and the list's entries become the comparison value. `field`: value is another field path, compared field-to-field. `relative_date`: value is a relative-date object, e.g. {"direction": "past", "unit": "days", "amount": 30}.""" + + score: float + """Required when operator is fuzzy_match: the 0-100 similarity threshold.""" + + group_index: int + """Optional grouped logic. When any condition carries group_index, conditions sharing an index form a group and grouped evaluation replaces evaluation_mode.""" + + group_logic: Literal["AND", "OR"] | str + """How conditions combine WITHIN a group.""" + + groups_logic: Literal["AND", "OR"] | str + """How group outcomes combine ACROSS groups. Read from the first condition.""" diff --git a/src/didit/types/components/transaction_rule_detail.py b/src/didit/types/components/transaction_rule_detail.py new file mode 100644 index 0000000..dbb4f33 --- /dev/null +++ b/src/didit/types/components/transaction_rule_detail.py @@ -0,0 +1,75 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.transaction_rule_action import TransactionRuleAction +from didit.types.components.transaction_rule_aggregation import TransactionRuleAggregation +from didit.types.components.transaction_rule_condition import TransactionRuleCondition +from didit.types.components.transaction_rule_scope import TransactionRuleScope + +__all__ = ["TransactionRuleDetail"] + + +class TransactionRuleDetail(BaseModel): + uuid: str | None = None + + title: str | None = None + """Rule name. Unique per application among non-deleted rules; duplicates return 400.""" + + description: str | None = None + + category: str | None = None + """Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce.""" + + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str | None = None + """ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped.""" + + evaluation_mode: Literal["ALL", "ANY"] | str | None = None + """Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index.""" + + scope: TransactionRuleScope | None = None + """Which transactions the rule applies to. Only these three keys are accepted.""" + + conditions: list[TransactionRuleCondition] | None = None + + aggregation: list[TransactionRuleAggregation] | None = None + + actions: list[TransactionRuleAction] | None = None + + metadata: dict[str, object] | None = None + """Free-form metadata stored on the rule.""" + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str | None = None + """Informational severity carried by library rules; not settable on custom rules.""" + + source: Literal["PRESET", "CUSTOM"] | str | None = None + """PRESET rules come from the library (install/uninstall, mode-only edits); CUSTOM rules are fully yours.""" + + library_key: str | None = None + """Stable library identifier; null for custom rules.""" + + latest_triggered_at: _dt.datetime | None = None + + run_count: int | None = None + """How many transactions matched this rule.""" + + approved_count: int | None = None + + reviewed_count: int | None = None + + declined_count: int | None = None + + awaiting_user_count: int | None = None + + approved_pct: float | None = None + + reviewed_pct: float | None = None + + declined_pct: float | None = None + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/transaction_rule_install_result.py b/src/didit/types/components/transaction_rule_install_result.py new file mode 100644 index 0000000..5064583 --- /dev/null +++ b/src/didit/types/components/transaction_rule_install_result.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["TransactionRuleInstallResult"] + + +class TransactionRuleInstallResult(BaseModel): + installed_library_keys: list[str] | None = None + """POST (install) only: presets installed by this call.""" + + installed_count: int | None = None + """POST (install) only.""" + + uninstalled_count: int | None = None + """DELETE (uninstall) only: presets removed by this call.""" diff --git a/src/didit/types/components/transaction_rule_library_entry.py b/src/didit/types/components/transaction_rule_library_entry.py new file mode 100644 index 0000000..45ee69b --- /dev/null +++ b/src/didit/types/components/transaction_rule_library_entry.py @@ -0,0 +1,47 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.transaction_rule_action import TransactionRuleAction +from didit.types.components.transaction_rule_aggregation import TransactionRuleAggregation +from didit.types.components.transaction_rule_condition import TransactionRuleCondition +from didit.types.components.transaction_rule_scope import TransactionRuleScope + +__all__ = ["TransactionRuleLibraryEntry"] + + +class TransactionRuleLibraryEntry(BaseModel): + library_key: str | None = None + + title: str | None = None + + description: str | None = None + + category: str | None = None + + evaluation_mode: Literal["ALL", "ANY"] | str | None = None + + scope: TransactionRuleScope | None = None + """Which transactions the rule applies to. Only these three keys are accepted.""" + + conditions: list[TransactionRuleCondition] | None = None + + aggregation: list[TransactionRuleAggregation] | None = None + + actions: list[TransactionRuleAction] | None = None + + metadata: dict[str, object] | None = None + + bundle: str | None = None + """Functional family, e.g. finance, aml_ctf, crypto_monitoring, travel_rule, responsible_gaming.""" + + tags: list[str] | None = None + + industries: list[str] | None = None + + source: str | None = None + + is_installed: bool | None = None + """Whether this preset is currently installed on the application.""" diff --git a/src/didit/types/components/transaction_rule_scope.py b/src/didit/types/components/transaction_rule_scope.py new file mode 100644 index 0000000..2c37aa0 --- /dev/null +++ b/src/didit/types/components/transaction_rule_scope.py @@ -0,0 +1,36 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit._models import BaseModel + +__all__ = ["TransactionRuleScope", "TransactionRuleScopeParam"] + + +class TransactionRuleScope(BaseModel): + """Which transactions the rule applies to. Only these three keys are accepted.""" + + transaction_types: list[str] | None = None + """Restrict to transaction types, e.g. finance, kyc, travel_rule, user_event. Empty or missing = no restriction.""" + + directions: list[Literal["INBOUND", "OUTBOUND"] | str] | None = None + """Restrict to transaction directions (case-insensitive on write).""" + + action_types: list[str] | None = None + """Restrict to your action_type values, e.g. withdrawal, deposit.""" + + +class TransactionRuleScopeParam(TypedDict, total=False): + """Which transactions the rule applies to. Only these three keys are accepted.""" + + transaction_types: Sequence[str] + """Restrict to transaction types, e.g. finance, kyc, travel_rule, user_event. Empty or missing = no restriction.""" + + directions: Sequence[Literal["INBOUND", "OUTBOUND"] | str] + """Restrict to transaction directions (case-insensitive on write).""" + + action_types: Sequence[str] + """Restrict to your action_type values, e.g. withdrawal, deposit.""" diff --git a/src/didit/types/components/travel_rule_inbound_response.py b/src/didit/types/components/travel_rule_inbound_response.py new file mode 100644 index 0000000..63bc397 --- /dev/null +++ b/src/didit/types/components/travel_rule_inbound_response.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.travel_rule_transfer_detail import TravelRuleTransferDetail + +__all__ = ["TravelRuleInboundResponse"] + + +class TravelRuleInboundResponse(BaseModel): + transaction_id: str | None = None + """Didit transaction UUID for the inbound deposit.""" + + txn_id: str | None = None + """Didit-generated txn_id for the sunrise transaction.""" + + created: bool | None = None + """true when a new transaction was minted (HTTP 201); false when an existing unclaimed deposit was reused (HTTP 200).""" + + travel_rule: TravelRuleTransferDetail | None = None + """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. originator_data/beneficiary_data are omitted while PII is masked (see description).""" diff --git a/src/didit/types/components/travel_rule_settings_detail.py b/src/didit/types/components/travel_rule_settings_detail.py new file mode 100644 index 0000000..bef37c2 --- /dev/null +++ b/src/didit/types/components/travel_rule_settings_detail.py @@ -0,0 +1,135 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "TravelRuleSettingsDetail", + "TravelRuleSettingsDetailNetworks", + "TravelRuleSettingsDetailNetworksGtr", + "TravelRuleSettingsDetailNetworksSygna", + "TravelRuleSettingsDetailNetworksTrust", + "TravelRuleSettingsDetailNetworksVerifyvasp", +] + + +class TravelRuleSettingsDetailNetworksGtr(BaseModel): + rail: Literal["GTR"] | str | None = None + + mode: Literal["own", "didit", "off", "unavailable"] | str | None = None + """own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available.""" + + platform_available: bool | None = None + """Whether platform membership is configured; not a live health check.""" + + connected: bool | None = None + """Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange.""" + + +class TravelRuleSettingsDetailNetworksSygna(BaseModel): + rail: Literal["SYGNA"] | str | None = None + + mode: Literal["own", "didit", "off", "unavailable"] | str | None = None + """own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available.""" + + platform_available: bool | None = None + """Whether platform membership is configured; not a live health check.""" + + connected: bool | None = None + """Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange.""" + + +class TravelRuleSettingsDetailNetworksTrust(BaseModel): + rail: Literal["TRUST"] | str | None = None + + mode: Literal["own", "didit", "off", "unavailable"] | str | None = None + """own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available.""" + + platform_available: bool | None = None + """Whether platform membership is configured; not a live health check.""" + + connected: bool | None = None + """Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange.""" + + +class TravelRuleSettingsDetailNetworksVerifyvasp(BaseModel): + rail: Literal["VERIFYVASP"] | str | None = None + + mode: Literal["own", "didit", "off", "unavailable"] | str | None = None + """own: connected customer membership takes precedence; didit: configured platform membership allowed; off: platform routing disabled with no own membership; unavailable: neither membership available.""" + + platform_available: bool | None = None + """Whether platform membership is configured; not a live health check.""" + + connected: bool | None = None + """Whether a connected customer membership exists. Credentials are validated locally; network validation occurs on a live exchange.""" + + +class TravelRuleSettingsDetailNetworks(BaseModel): + """Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check.""" + + gtr: TravelRuleSettingsDetailNetworksGtr | None = None + + trust: TravelRuleSettingsDetailNetworksTrust | None = None + + verifyvasp: TravelRuleSettingsDetailNetworksVerifyvasp | None = None + + sygna: TravelRuleSettingsDetailNetworksSygna | None = None + + +class TravelRuleSettingsDetail(BaseModel): + is_enabled: bool | None = None + """Whether the managed Travel Rule negotiation flow is active for outbound `travelRule` transactions on this application.""" + + jurisdiction: str | None = None + """Your VASP's operating jurisdiction code. Default `"EU"`.""" + + name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] | str | None = None + """How strictly beneficiary names must match before a transfer can reach COMPLETED.""" + + confirmation_timeout_hours: int | None = None + """Hours an AWAITING_COUNTERPARTY transfer may remain unconfirmed before the periodic sweeper expires it. Default 48.""" + + timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str | None = None + """What happens to the underlying transaction when a transfer expires. Default HOLD.""" + + threshold_amount: str | None = None + """Decimal string. Minimum transfer amount that triggers a managed exchange; transfers below it resolve straight to NOT_APPLICABLE. Defaults to "0.00" (EU TFR applies no minimum amount).""" + + legal_name: str | None = None + """Your VASP's legal name. Required (non-blank) to set is_enabled to true.""" + + lei: str | None = None + """Your Legal Entity Identifier, surfaced in the VASP directory.""" + + compliance_email: str | None = None + """Compliance contact email for your VASP profile.""" + + is_discoverable: bool | None = None + """Whether your VASP profile appears in GET /v3/travel-rule/vasps/ search results and can be reached on the INTERNAL rail. Default true.""" + + inbound_auto_accept: bool | None = None + """When true, inbound transfers whose beneficiary wallet is a verified address-book entry with a matching name are accepted automatically, without a manual review step. Default false.""" + + allow_self_declaration: bool | None = None + """When true, the wallet-ownership widget offers self-declaration as a fallback proof method. Default false.""" + + allow_screenshot_proof: bool | None = None + """When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true.""" + + auto_wallet_verification: bool | None = None + """When true, submitting an outbound Travel Rule transfer that needs end-user proof of wallet control auto-mints a wallet-ownership widget session bound to the transaction and exposes it in the transaction's action_required block. Default true.""" + + vasp_attribution_enabled: bool | None = None + """When true, an outbound Travel Rule transfer that no rail can route is attributed through blockchain analytics: the destination wallet is resolved to its owning VASP, the entity is recorded on the transfer and in the VASP directory, and the rails are re-run once with the attributed name. Default true.""" + + travel_address: str | None = None + """Read-only. Your VASP's own travel address - the encoded inbound TRP inquiry endpoint other VASPs use to reach you. Populated once your VASP profile is complete.""" + + disabled_networks: list[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] | None = None + """Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.""" + + networks: TravelRuleSettingsDetailNetworks | None = None + """Effective membership configuration for each network. No network, including GTR, is included by default. This is not a live health or delivery check.""" diff --git a/src/didit/types/components/travel_rule_transfer_detail.py b/src/didit/types/components/travel_rule_transfer_detail.py new file mode 100644 index 0000000..890fce0 --- /dev/null +++ b/src/didit/types/components/travel_rule_transfer_detail.py @@ -0,0 +1,79 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "TravelRuleTransferDetail", + "TravelRuleTransferDetailCounterpartyVasp", + "TravelRuleTransferDetailTimeline", +] + + +class TravelRuleTransferDetailCounterpartyVasp(BaseModel): + name: str | None = None + + lei: str | None = None + + dd_score: int | None = None + + +class TravelRuleTransferDetailTimeline(BaseModel): + status: str | None = None + + at: _dt.datetime | None = None + + detail: str | None = None + + +class TravelRuleTransferDetail(BaseModel): + """Full transfer object returned by the ownership-confirmation and finish/cancel endpoints. originator_data/beneficiary_data are omitted while PII is masked (see description).""" + + uuid: str | None = None + + status: ( + Literal[ + "AWAITING_COUNTERPARTY", + "ON_HOLD", + "COMPLETED", + "FINISHED", + "CANCELLED", + "EXPIRED", + "COUNTERPARTY_MISMATCHED_DATA", + "COUNTERPARTY_VASP_NOT_FOUND", + "COUNTERPARTY_VASP_NOT_REACHABLE", + "UNCONFIRMED_OWNERSHIP", + "COUNTERPARTY_VASP_GENERAL_DECLINE", + "NOT_ENOUGH_COUNTERPARTY_DATA", + "NOT_APPLICABLE", + ] + | str + | None + ) = None + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + + rail: Literal["INTERNAL", "TRP", "GTR", "EMAIL"] | str | None = None + + protocol: str | None = None + + ivms_version: str | None = None + + required: bool | None = None + + ownership_confirmed: bool | None = None + + deadline_at: _dt.datetime | None = None + + timeline: list[TravelRuleTransferDetailTimeline] | None = None + + counterparty_vasp: TravelRuleTransferDetailCounterpartyVasp | None = None + + originator_data: dict[str, object] | None = None + + beneficiary_data: dict[str, object] | None = None + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/travel_rule_widget_session_response.py b/src/didit/types/components/travel_rule_widget_session_response.py new file mode 100644 index 0000000..b4ef5f5 --- /dev/null +++ b/src/didit/types/components/travel_rule_widget_session_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["TravelRuleWidgetSessionResponse"] + + +class TravelRuleWidgetSessionResponse(BaseModel): + widget_session_id: str | None = None + + token: str | None = None + """Opaque session token embedded in the widget URL.""" + + url: str | None = None + """The hosted wallet-ownership widget URL. Open it verbatim in the system browser (never inside a mobile SDK webview).""" + + expires_at: _dt.datetime | None = None diff --git a/src/didit/types/components/user_detail_item.py b/src/didit/types/components/user_detail_item.py new file mode 100644 index 0000000..1e147b3 --- /dev/null +++ b/src/didit/types/components/user_detail_item.py @@ -0,0 +1,64 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.user_list_item import UserListItem + +__all__ = ["UserDetailItem", "UserDetailItemComments"] + + +class UserDetailItemComments(BaseModel): + uuid: str | None = None + + comment_type: ( + Literal[ + "COMMENT", + "STATUS_CHANGED", + "METADATA_UPDATED", + "TAG_ADDED", + "TAG_REMOVED", + "PROFILE_UPDATED", + ] + | str + | None + ) = None + + comment: str | None = None + + actor_email: str | None = None + + actor_name: str | None = None + + previous_status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + + new_status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + + previous_value: dict[str, object] | None = None + """Previous field values for PROFILE_UPDATED entries.""" + + new_value: dict[str, object] | None = None + """New field values for PROFILE_UPDATED entries.""" + + changed_fields: list[str] | None = None + """Names of the fields changed by a PROFILE_UPDATED entry.""" + + metadata: dict[str, object] | None = None + + mentioned_emails: list[str] | None = None + + created_at: _dt.datetime | None = None + + +class UserDetailItem(UserListItem): + """Full user detail. Extends UserListItem with metadata, comments, and updated_at.""" + + metadata: dict[str, object] | None = None + """Custom metadata JSON you attached to this user. Defaults to `{}`.""" + + comments: list[UserDetailItemComments] | None = None + """Activity log and comments for this user (status changes, profile edits, manual notes).""" + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/components/user_list_item.py b/src/didit/types/components/user_list_item.py new file mode 100644 index 0000000..d3400d0 --- /dev/null +++ b/src/didit/types/components/user_list_item.py @@ -0,0 +1,91 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["UserListItem", "UserListItemFeaturesList", "UserListItemTags"] + + +class UserListItemFeaturesList(BaseModel): + feature: str | None = None + + status: str | None = None + + +class UserListItemTags(BaseModel): + uuid: str | None = None + + name: str | None = None + + color: str | None = None + + +class UserListItem(BaseModel): + """A verified user.""" + + didit_internal_id: str | None = None + """Didit's stable internal UUID for this user.""" + + vendor_data: str | None = None + """Your unique identifier for this user (passed when creating sessions). This can be null when no vendor identifier was supplied.""" + + display_name: str | None = None + """Custom display name set by you""" + + full_name: str | None = None + """Full name extracted from verified documents""" + + date_of_birth: _dt.date | None = None + + effective_name: str | None = None + """Best available name: display_name if set, otherwise full_name""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str | None = None + """Lifecycle status of the user record (NOT a session status). `ACTIVE` is the default, `FLAGGED` marks the user for manual attention, `BLOCKED` prevents new sessions for this `vendor_data`.""" + + portrait_image_url: str | None = None + """Presigned URL of the user's portrait photo (expires after a few hours)""" + + session_count: int | None = None + """Total number of verification sessions for this user""" + + approved_count: int | None = None + """Number of approved sessions""" + + declined_count: int | None = None + """Number of declined sessions""" + + in_review_count: int | None = None + """Number of sessions in review""" + + issuing_states: list[str] | None = None + """ISO 3166-1 alpha-3 codes of issuing countries seen on this user's approved ID documents, e.g. `["USA", "ESP"]`. Empty array when none.""" + + approved_emails: list[str] | None = None + """Verified email addresses collected from this user's approved sessions, e.g. `["john@example.com"]`.""" + + approved_phones: list[str] | None = None + """Verified phone numbers collected from this user's approved sessions, e.g. `["+14155551234"]`.""" + + features: dict[str, object] | None = None + """Aggregated per-feature status across all of this user's sessions. Possible keys: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `QUESTIONNAIRE`, `EMAIL_VERIFICATION`, `PHONE`, `AML`, `IP_ANALYSIS`, `AGE_ESTIMATION`, `DATABASE_VALIDATION`. Possible values: `Approved`, `Declined`, `In Review`, `Not Finished`, `Resub Requested`.""" + + features_list: list[UserListItemFeaturesList] | None = None + """Same data as `features`, as an ordered array of `{feature, status}` objects.""" + + last_session_at: _dt.datetime | None = None + """Timestamp of the most recent session""" + + first_session_at: _dt.datetime | None = None + """Timestamp of the first session""" + + last_activity_at: _dt.datetime | None = None + """Timestamp of the most recent activity on this user (session, transaction, status change, data edit, etc.).""" + + tags: list[UserListItemTags] | None = None + """Tags assigned to this user""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/components/vasp_directory_entry.py b/src/didit/types/components/vasp_directory_entry.py new file mode 100644 index 0000000..ad2f82e --- /dev/null +++ b/src/didit/types/components/vasp_directory_entry.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["VaspDirectoryEntry"] + + +class VaspDirectoryEntry(BaseModel): + name: str | None = None + + lei: str | None = None + + jurisdiction: str | None = None + + dd_status: Literal["NOT_STARTED", "IN_PROGRESS", "COMPLETED", "REJECTED"] | str | None = None + + dd_score: int | None = None + + reachable_rails: list[Literal["INTERNAL", "TRP", "GTR", "EMAIL"] | str] | None = None + + is_didit_customer: bool | None = None + """true for discoverable Didit applications (dd_status is always COMPLETED, dd_score always 0 for these); false for internal counterparty-registry rows, which carry a real due-diligence status and score.""" + + dd_score_breakdown: dict[str, int] | None = None + """Per-component contributions to dd_score (licensing/registry, sanctions/PEP, jurisdiction risk, on-chain analytics risk), all derived from checks Didit runs in-house. Empty for Didit-customer rows.""" + + dd_assessed_at: _dt.datetime | None = None + """When the due-diligence assessment last ran. Null for Didit-customer rows.""" diff --git a/src/didit/types/components/verify_email_response.py b/src/didit/types/components/verify_email_response.py new file mode 100644 index 0000000..78b7d03 --- /dev/null +++ b/src/didit/types/components/verify_email_response.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.application_credentials_summary import ApplicationCredentialsSummary +from didit.types.components.organization_summary import OrganizationSummary + +__all__ = ["VerifyEmailResponse"] + + +class VerifyEmailResponse(BaseModel): + access_token: str | None = None + """RS256-signed JWT. Use as `Authorization: Bearer ` for Account Management endpoints on `apx.didit.me/auth/v2`.""" + + refresh_token: str | None = None + """Refresh token paired with the access token.""" + + expires_in: int | None = None + """Lifetime of the access token in seconds (default 86400).""" + + organization: OrganizationSummary | None = None + """Minimal organization shape returned inline by `POST /programmatic/verify-email/`.""" + + application: ApplicationCredentialsSummary | None = None + """Minimal application shape returned inline by `POST /programmatic/verify-email/`. Persist the `api_key` immediately.""" diff --git a/src/didit/types/components/wallet_address_entry.py b/src/didit/types/components/wallet_address_entry.py new file mode 100644 index 0000000..e78c70c --- /dev/null +++ b/src/didit/types/components/wallet_address_entry.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["WalletAddressEntry"] + + +class WalletAddressEntry(BaseModel): + uuid: str | None = None + + address: str | None = None + """The wallet address. Unique per application and chain among non-deleted entries.""" + + chain: str | None = None + """Free-text chain identifier, e.g. "ethereum", "bitcoin".""" + + holder_vendor_data: str | None = None + """Your internal identifier for the holder.""" + + holder_name: str | None = None + """Name of the wallet holder, used for the beneficiary name match.""" + + entity_type: str | None = None + """Free text, defaults to "individual".""" + + is_ownership_verified: bool | None = None + """Read-only. Whether ownership of this address has been proven.""" + + created_at: _dt.datetime | None = None + + self_declared: bool | None = None + """Whether the entry was created as a self-declared ownership proof.""" + + travel_address: str | None = None + """The counterparty VASP's travel address for this wallet, if known. Lets Didit route this beneficiary's transfers over the TRP rail.""" diff --git a/src/didit/types/components/wallet_address_entry_update.py b/src/didit/types/components/wallet_address_entry_update.py new file mode 100644 index 0000000..7ff137b --- /dev/null +++ b/src/didit/types/components/wallet_address_entry_update.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["WalletAddressEntryUpdate"] + + +class WalletAddressEntryUpdate(BaseModel): + """PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, and travel_address.""" + + holder_name: str | None = None + + holder_vendor_data: str | None = None + + entity_type: str | None = None + + travel_address: str | None = None diff --git a/src/didit/types/components/wallet_screening_result.py b/src/didit/types/components/wallet_screening_result.py new file mode 100644 index 0000000..b97081f --- /dev/null +++ b/src/didit/types/components/wallet_screening_result.py @@ -0,0 +1,281 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "WalletScreeningResult", + "WalletScreeningResultCounterpartyConnections", + "WalletScreeningResultDestinationOfFunds", + "WalletScreeningResultRiskFactors", + "WalletScreeningResultSourceOfFunds", +] + + +class WalletScreeningResultCounterpartyConnections(BaseModel): + entity_name: str | None = None + """Attributed entity name.""" + + entity_type: str | None = None + """Provider entity type.""" + + entity_subtype: str | None = None + """Provider entity subtype.""" + + risk_level: str | None = None + """Lowercase and provider-dependent. For Merkle Science it is the address's own 0-5 risk grade (`no_risk`, `low`, `medium_low`, `medium`, `high`, `critical`), raised to `high` when the entity's category is high risk; it is not a separate grade of the entity.""" + + categories: list[str] | None = None + """Normalised risk categories of the entity.""" + + received_usd: float | None = None + """USD received from the entity, when the provider reports it.""" + + sent_usd: float | None = None + """USD sent to the entity, when the provider reports it.""" + + received_hops: int | None = None + """Hops to the entity on the receiving side, when the provider reports them.""" + + sent_hops: int | None = None + """Hops to the entity on the sending side, when the provider reports them.""" + + percentage: float | None = None + """Share of the address's exposure, when the provider reports it; otherwise 0.""" + + is_direct: bool | None = None + """Always `true` for Merkle Science, whose connections are attributions rather than counterparties.""" + + country: str | None = None + """Country of the entity, when known.""" + + provider_entity_id: str | None = None + """The provider's own identifier for the entity.""" + + +class WalletScreeningResultDestinationOfFunds(BaseModel): + category: str | None = None + """Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`.""" + + percentage: float | None = None + """Share of the direction's total exposure, 0-100.""" + + amount_usd: float | None = None + """USD value attributed to this entity.""" + + entity_name: str | None = None + """Name of the attributed entity, for example an exchange, a mixer or a sanctioned group.""" + + entity_type: str | None = None + """Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`).""" + + entity_subtype: str | None = None + """More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`).""" + + hops: int | None = None + """Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`.""" + + description: str | None = None + """`entity_type / entity_subtype`, as the provider labels the entity.""" + + is_direct: bool | None = None + """True when `exposure_type` is `direct`.""" + + exposure_direction: Literal["incoming", "outgoing", "connected"] | str | None = None + """Whether the funds came from (`incoming`) or went to (`outgoing`) this entity.""" + + exposure_type: Literal["direct", "indirect"] | str | None = None + """`direct` (1 hop) or `indirect` (multi-hop) exposure.""" + + provider_entity_id: str | None = None + """The provider's own identifier for the entity.""" + + provider_entity_type_id: str | None = None + """The provider's own identifier for `entity_type`.""" + + provider_entity_subtype_id: str | None = None + """The provider's own identifier for `entity_subtype`.""" + + country: str | None = None + """Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it.""" + + raw_amount: float | None = None + """Amount in the asset's own unit, when the provider reports it.""" + + raw_currency: str | None = None + """Unit of `raw_amount`, when the provider reports it.""" + + +class WalletScreeningResultRiskFactors(BaseModel): + source: Literal["attribution", "exposure"] | str | None = None + """`attribution` is what the provider says the address itself is (its own tag or cluster); `exposure` is where its funds actually moved.""" + + category: str | None = None + """Normalised risk category, same vocabulary as the exposure breakdown.""" + + label: str | None = None + """The provider's own wording for the driver (e.g. `Gambling / Crypto only`), which is what you would also see in the provider's console.""" + + entity_name: str | None = None + """Attributed entity behind the driver, for example a gambling site or an exchange.""" + + direction: ( + Literal["incoming", "outgoing", "connected", "unknown", "attribution"] | str | None + ) = None + """Direction of the fund flow, `attribution` when the driver is the address's own tag, or `unknown` when the provider did not report a direction.""" + + exposure_type: Literal["direct", "indirect", "unknown", "attribution"] | str | None = None + """`direct` (1 hop) or `indirect` (multi-hop), `attribution` for the address's own tag, or `unknown` when the provider did not state how many hops away the counterparty is. Treat `unknown` as unclassified, not as `direct`.""" + + amount_usd: float | None = None + """USD value attributed to this driver.""" + + percentage: float | None = None + """Share of the address's TOTAL screened volume, both directions combined - unlike `percentage` inside `source_of_funds`/`destination_of_funds`, which is normalised per direction. Comparable and summable across the whole list.""" + + is_high_risk: bool | None = None + """Whether the category is one Didit treats as high risk (`sanctioned`, `mixer`, `stolen_funds`, `ransomware`, `scam`, `darknet_market`, `terrorist_financing`, `child_exploitation`, `hacked_exchange`, `high_risk_exchange`, `seized_funds`).""" + + description: str | None = None + """One-line rendering of the driver, e.g. `9.01% indirect outgoing exposure to Gambling / Crypto only (Example Gambling)`.""" + + +class WalletScreeningResultSourceOfFunds(BaseModel): + category: str | None = None + """Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`.""" + + percentage: float | None = None + """Share of the direction's total exposure, 0-100.""" + + amount_usd: float | None = None + """USD value attributed to this entity.""" + + entity_name: str | None = None + """Name of the attributed entity, for example an exchange, a mixer or a sanctioned group.""" + + entity_type: str | None = None + """Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`).""" + + entity_subtype: str | None = None + """More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`).""" + + hops: int | None = None + """Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`.""" + + description: str | None = None + """`entity_type / entity_subtype`, as the provider labels the entity.""" + + is_direct: bool | None = None + """True when `exposure_type` is `direct`.""" + + exposure_direction: Literal["incoming", "outgoing", "connected"] | str | None = None + """Whether the funds came from (`incoming`) or went to (`outgoing`) this entity.""" + + exposure_type: Literal["direct", "indirect"] | str | None = None + """`direct` (1 hop) or `indirect` (multi-hop) exposure.""" + + provider_entity_id: str | None = None + """The provider's own identifier for the entity.""" + + provider_entity_type_id: str | None = None + """The provider's own identifier for `entity_type`.""" + + provider_entity_subtype_id: str | None = None + """The provider's own identifier for `entity_subtype`.""" + + country: str | None = None + """Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it.""" + + raw_amount: float | None = None + """Amount in the asset's own unit, when the provider reports it.""" + + raw_currency: str | None = None + """Unit of `raw_amount`, when the provider reports it.""" + + +class WalletScreeningResult(BaseModel): + provider: str + """Provider that performed the screening (e.g. `merklescience`, `crystal`).""" + + screening_type: Literal["WALLET_SCREENING"] | str + """Always `WALLET_SCREENING` for this endpoint.""" + + risk_score: int + """Normalised 0-100 risk score. Higher means greater exposure to risky entities. Merkle Science grades an address 0-5, which maps to 0, 15, 35, 55, 75 or 95.""" + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str + """Risk bucket derived from `risk_score`: `0-9` UNKNOWN, `10-39` LOW, `40-69` MEDIUM, `70-89` HIGH, `90-100` CRITICAL. `UNKNOWN` is the lowest band, not a separate no-data state: `risk_score` 0 (the common clean-address case) means no adverse assessment, while a non-zero score in the 1-9 range is a real but sub-LOW signal - read the `risk_score`, not just the band. Never treat `UNKNOWN` as an affirmative low-risk or clear rating; do not display it as a pass.""" + + status: Literal["SCREENED", "PENDING", "ERROR"] | str + """Screening outcome status.""" + + summary: str + """Human-readable summary of the screening result.""" + + wallet_address: str + """The screened address, echoed back.""" + + transaction_hash: str | None = None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + blockchain: str + """The blockchain that was screened.""" + + block_timestamp: str | None = None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + total_value_usd: float | None = None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + fee_usd: float | None = None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + sender_rows: list[dict[str, object]] | None = None + """Transaction screening field, shared with the result shape. Always empty on a wallet screening result.""" + + receiver_rows: list[dict[str, object]] | None = None + """Transaction screening field, shared with the result shape. Always empty on a wallet screening result.""" + + transaction_flows: list[dict[str, object]] | None = None + """Transaction screening field, shared with the result shape. Always empty on a wallet screening result.""" + + transaction_flows_status: str | None = None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + transaction_flows_error: str | None = None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + source_of_funds: list[WalletScreeningResultSourceOfFunds] + """Where the address received funds from, attributed by entity. Each entry is an exposure breakdown.""" + + destination_of_funds: list[WalletScreeningResultDestinationOfFunds] + """Where the address sent funds to, attributed by entity. Same item shape as `source_of_funds` with `exposure_direction` = `outgoing`.""" + + counterparty_connections: list[WalletScreeningResultCounterpartyConnections] + """Entities the provider attributes the screened address to. For Merkle Science these come from the address's own owner and user tags, so the same entity can appear twice, and they carry no fund flows: `received_usd`, `sent_usd`, `received_hops` and `sent_hops` are `null` and `percentage` is 0. Do not route on `risk_level` or `is_direct` here: use `sanctions_hit`, `dominant_risk_category` and `risk_factors[].is_high_risk` for the verdict, and `exposure_type` in `source_of_funds`/`destination_of_funds` for directness.""" + + network_nodes: list[dict[str, object]] | None = None + """Exposure graph nodes for transaction screening. Empty on a wallet screening result.""" + + network_edges: list[dict[str, object]] | None = None + """Exposure graph edges for transaction screening. Empty on a wallet screening result.""" + + sanctions_hit: bool + """True if the address has direct or indirect sanctions exposure.""" + + pep_counterparty: bool | None = None + """Politically exposed person exposure. `null` when no PEP finding is available, which is every result today because no provider reports one; `false` only for an explicit negative screening result. Do not read `null` as a completed PEP check.""" + + dominant_risk_category: str | None + """Highest-weighted high-risk category, or `null` when none is dominant (e.g. `sanctioned`, `mixer`, `stolen_funds`).""" + + risk_factors: list[WalletScreeningResultRiskFactors] | None = None + """Ranked explanation of `risk_score`: what produced this verdict. Providers grade an address on their own model and return only a verdict (Merkle Science returns an integer 0-5), so the score alone is not actionable. Ordered high-risk first, then by the value that moved through each driver, capped at the five strongest; `source_of_funds` and `destination_of_funds` always carry the complete breakdown. A high `risk_score` with no `is_high_risk` entry means the provider reported no sanctions or attributed-entity match and graded the address purely on its own exposure model - `summary` states this explicitly.""" + + raw_response: dict[str, object] | None = None + """The provider's own payload. Its shape is provider-specific and can change without notice: read the normalised fields instead of parsing it.""" + + report_signature: str + """Opaque HMAC-SHA256 signature over this exact result, scoped to your application. Not meaningful on its own - repost the full response body, unmodified and including this field, to `POST /v3/wallet-screening/pdf/` to render it as a PDF. The PDF endpoint rejects the request with `400` if `report_signature` is missing or if any signed field (e.g. `risk_score`, `severity`, `sanctions_hit`) was edited before repost.""" diff --git a/src/didit/types/components/webhook_event.py b/src/didit/types/components/webhook_event.py new file mode 100644 index 0000000..502cd61 --- /dev/null +++ b/src/didit/types/components/webhook_event.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypeAlias + +__all__ = ["WebhookEvent"] + + +WebhookEvent: TypeAlias = ( + Literal[ + "status.updated", + "data.updated", + "user.status.updated", + "user.data.updated", + "business.status.updated", + "business.data.updated", + "activity.created", + "transaction.created", + "transaction.status.updated", + "travel_rule.status.updated", + ] + | str +) +"""Supported webhook event name. Use these exact strings in `subscribed_events`; unsupported values are rejected.""" diff --git a/src/didit/types/components/webhook_subscribed_events.py b/src/didit/types/components/webhook_subscribed_events.py new file mode 100644 index 0000000..05fc593 --- /dev/null +++ b/src/didit/types/components/webhook_subscribed_events.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypeAlias + +from didit.types.components.webhook_event import WebhookEvent + +__all__ = ["WebhookSubscribedEvents", "WebhookSubscribedEventsParam"] + + +WebhookSubscribedEvents: TypeAlias = list[WebhookEvent] +"""Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + +WebhookSubscribedEventsParam: TypeAlias = Sequence[WebhookEvent] +"""Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" diff --git a/src/didit/types/components/workflow_list_item.py b/src/didit/types/components/workflow_list_item.py new file mode 100644 index 0000000..61421cc --- /dev/null +++ b/src/didit/types/components/workflow_list_item.py @@ -0,0 +1,87 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["WorkflowListItem", "WorkflowListItemCreatedBy"] + + +class WorkflowListItemCreatedBy(BaseModel): + """Who created this version: `{name, email, type}` where `type` is `user` (Console user, `email` set) or `api_key` (`name` is the client id, `email` is null). `null` when no creator was recorded.""" + + name: str | None = None + + email: str | None = None + + type: Literal["user", "api_key"] | str | None = None + + +class WorkflowListItem(BaseModel): + """One workflow version in the list view.""" + + uuid: str | None = None + """Per-version UUID. Use as `{settings_uuid}` for get/update/delete, and as `workflow_id` when creating sessions.""" + + workflow_id: str | None = None + """Stable group ID shared by every version of the same workflow. Equals `uuid` for version 1.""" + + workflow_label: str | None = None + """Display name for the workflow.""" + + workflow_type: str | None = None + """Base type for simple workflows: `kyc`, `kyb`, `adaptive_age_verification`, `biometric_authentication`, etc. `null` for graph-based workflows without a KYB feature.""" + + is_default: bool | None = None + """Whether this is the default workflow for new sessions.""" + + is_archived: bool | None = None + + is_white_label_enabled: bool | None = None + """Whether white-label customization applies to sessions created with this workflow.""" + + total_price: float | None = None + """Fixed price per verification in USD (JSON number, excludes variable-priced features such as DATABASE_VALIDATION and PHONE_VERIFICATION).""" + + min_price: float | None = None + """Cheapest fixed price across workflow-graph paths, in USD.""" + + max_price: float | None = None + """Most expensive fixed price across workflow-graph paths, in USD.""" + + features: str | None = None + """Enabled features as a single `" + "`-joined string, e.g. `"OCR + LIVENESS + FACE_MATCH"`. Split on `" + "` to get a list.""" + + is_simple_workflow: bool | None = None + """True when the workflow has no custom workflow graph.""" + + is_editable: bool | None = None + """True when this version is a `draft` (drafts are editable in place; published versions are immutable in the Console).""" + + workflow_url: str | None = None + """Public shareable verification URL for this workflow (`https://verify.didit.me/u/`).""" + + max_retry_attempts: int | None = None + """Maximum retry attempts allowed after a declined verification (0-10).""" + + retry_window_days: int | None = None + """Rolling window in days used to count retries (1-365). `null` = all-time limit.""" + + session_expiration_time: int | None = None + """Seconds before an unfinished session expires (3600-2419200).""" + + version: int | None = None + """Version number within the `workflow_id` group, starting at 1.""" + + status: Literal["draft", "published"] | str | None = None + """Version status. New sessions use the latest `published` version of a group.""" + + has_draft: bool | None = None + """True when any version in this `workflow_id` group is currently a draft.""" + + created_by: WorkflowListItemCreatedBy | None = None + """Who created this version: `{name, email, type}` where `type` is `user` (Console user, `email` set) or `api_key` (`name` is the client id, `email` is null). `null` when no creator was recorded.""" + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/customization/__init__.py b/src/didit/types/customization/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/customization/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/customization/update_params.py b/src/didit/types/customization/update_params.py new file mode 100644 index 0000000..1974825 --- /dev/null +++ b/src/didit/types/customization/update_params.py @@ -0,0 +1,99 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +from didit._types import FileTypes + +__all__ = ["CustomizationUpdateParams"] + + +class CustomizationUpdateParams(TypedDict, total=False): + """Partial branding update body. Provide only the fields you want to change. Hex colors accept `#RRGGBB` or `#RRGGBBAA`. Custom domain and white-label email are not accepted here.""" + + color_primary: str + """Headers / primary text color.""" + + color_secondary: str + """Paragraph / secondary text color.""" + + color_background: str + """Page background color.""" + + color_panel: str + """Panel / accent color.""" + + color_panel_10: str + """Panel color at 10% opacity.""" + + color_on_panel_1: str + """Support text color on panels.""" + + color_on_panel_2: str + """Pill / badge color on panels.""" + + color_on_background: str + """Text color on background.""" + + color_button_1: str + """Primary button color.""" + + color_button_2: str + """Secondary button color.""" + + color_button_text_1: str + """Primary button text color.""" + + color_button_text_2: str + """Secondary button text color.""" + + color_pill_text: str + """Text color for pills, inputs and selectors.""" + + border_radius_panel: int + """Panel corner radius in pixels.""" + + border_radius_buttons: int + """Button corner radius in pixels.""" + + font_family: str + """Google Font family name (e.g. `Inter`, `AR One Sans`).""" + + font_weight: str + """Font weight available for the selected family (e.g. `regular`, `500`, `700`).""" + + app_public_name: str | None + """Public name shown in the verification flow.""" + + privacy_policy_url: str | None + """Privacy policy URL shown to the end user.""" + + skip_welcome_screen: bool + """Skip the initial welcome screen.""" + + disable_login_with_didit: bool + """Continue as guest instead of showing the login-with-Didit screen. Cannot be set back to `false` while a custom domain is configured (400).""" + + hide_progress_bar: bool + """Hide the progress bar during the flow.""" + + callback_seconds: int + """Delay (seconds) before redirecting to the callback URL.""" + + delete_logo_square: bool + """When `true`, removes the current square logo.""" + + delete_logo_rectangular: bool + """When `true`, removes the current rectangular logo.""" + + delete_favicon: bool + """When `true`, removes the current favicon.""" + + image_square: FileTypes + """Square logo (1:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`.""" + + image_rectangular: FileTypes + """Rectangular logo (2:1 aspect ratio). Max **1 MB**. Allowed: `png`, `jpg`, `jpeg`, `webp`.""" + + image_favicon: FileTypes + """Favicon image. Max **500 KB**.""" diff --git a/src/didit/types/database_validation/__init__.py b/src/didit/types/database_validation/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/database_validation/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/database_validation/validate_params.py b/src/didit/types/database_validation/validate_params.py new file mode 100644 index 0000000..096b931 --- /dev/null +++ b/src/didit/types/database_validation/validate_params.py @@ -0,0 +1,227 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["DatabaseValidationValidateParams", "DatabaseValidationValidateParamsAddressVariant1"] + + +class DatabaseValidationValidateParamsAddressVariant1(TypedDict, total=False): + street_1: str + + street_2: str + + city: str + + region: str + + postal_code: str + + country: str + + +class DatabaseValidationValidateParams(TypedDict, total=False): + issuing_state: Required[str] + """ISO 3166-1 **alpha-3** country code of the registry to validate against (e.g. `BRA`, `ESP`, `COL`). Determines which services are available. Unsupported codes return `400` with the full list of valid options.""" + + services: Sequence[str] + """Catalog `service_id`s to run for this country (e.g. `["bra_cpf"]`). Also accepts a single string or a comma-separated/JSON-encoded string. **If omitted or empty, exactly one default service runs** - the longest-established live service for the country; newer, biometric, and pay-as-you-go services must be named explicitly. Sending `services` switches the response to the extended shape (adds `services_used` and `match_score`). Every id must exist and be live for the `issuing_state`; services that require onboarding return `400` until activated for your organization. Discover available services per country via `GET /v1/organization/database-validation-countries/` (a catalog endpoint not documented in this spec) or the Business Console workflow editor.""" + + validation_type: str + """Deprecated. DEPRECATED. Accepted for backward compatibility but ignored - the response `validation_type` is now derived from how many services full-matched. Use `services` to pin specific services.""" + + consent: bool + """Set to `true` when the end user has explicitly consented to the selected validation services. Required for services flagged `requires_consent=true` in the catalog - without it those services return `400` with `services_requiring_consent`.""" + + identification_number: str + """Universal identification number - automatically mapped to the correct country-specific field, so you can use it instead of `personal_number`/`tax_number`/`document_number`: ARG→document_number (DNI), BOL→document_number (CI), BRA→tax_number (CPF, 11 digits), CHL→personal_number (RUT), COL→personal_number (Cédula), CRI→personal_number (Cédula), DOM→personal_number (Cédula, 11 digits), ECU→personal_number (Cédula, 10 digits), ESP→personal_number (DNI/NIE), GTM→document_number (DPI), HND→document_number (DNI), MEX→personal_number (CURP, 18 chars), PAN→personal_number (Cédula), PER→personal_number (DNI, 8 digits), PRY→document_number (CI), SLV→document_number (DUI), URY→personal_number (CI), VEN→document_number (Cédula). It never overrides an explicitly provided country-specific field.""" + + first_name: str + """The individual's first name. Required by some services/countries.""" + + last_name: str + """The individual's last name. Required by some services/countries.""" + + middle_name: str + """Middle name, used by some country services (AUS, NZL, …).""" + + full_name: str + """Full name - some services accept this in lieu of first/last name (e.g. CHN services, which match the native-script name).""" + + date_of_birth: str | _dt.date + """Date of birth, `YYYY-MM-DD`. Required by many services.""" + + personal_number: str + """Government-issued unique personal identifier. Used by: CHL (RUT), COL (Cédula), CRI, DOM, ECU, ESP (DNI/NIE), MEX (CURP), PAN (Cédula), PER (DNI), URY. Consider `identification_number` instead.""" + + tax_number: str + """Tax identification number. Used by: BRA (CPF, 11 digits). Consider `identification_number` instead.""" + + document_number: str + """Document number. Used by: ARG (DNI), BOL (CI), GTM (DPI), HND (DNI), PRY (CI), SLV (DUI), VEN (Cédula). Consider `identification_number` instead.""" + + document_type: ( + Literal["P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER"] | str + ) + """Type of document being validated: `P` passport, `DL` driver license, `ID` national ID, `RP` residence permit (plus `SSC`, `HIC`, `WP`, `TC`, `VISA`, `PSC`, `BC`, `OTHER`). Required by some services (e.g. ESP).""" + + expiration_date: str | _dt.date + """Document expiration date, `YYYY-MM-DD`. Required for ESP (Spain) validation.""" + + date_of_issue: str | _dt.date + """Document issue date, `YYYY-MM-DD` (fecha de expedición). Required for COL (Colombia) cédula validation.""" + + nationality: str + """Nationality as ISO 3166-1 alpha-3. Required by some services.""" + + gender: Literal["M", "F", "X"] | str + """Gender: `M`, `F`, or `X` (other/unknown). Required for Argentina's RENAPER validation (`arg_renaper`).""" + + address: DatabaseValidationValidateParamsAddressVariant1 | str + """Residential address. Prefer the structured object `{"street_1":"123 Main St","street_2":"Apt 4B","city":"Springfield","region":"IL","postal_code":"62701","country":"US"}`; a complete single-line string is still accepted. Required by address-verification services; when a service defines address requirements, missing parts return `400` with `address_fields_required_by`.""" + + address_element_1: str + """Street address including street number and type. If omitted, derived from `address`.""" + + address_element_2: str + """Apartment, unit, building, floor, or extra address line. Only send when you have it explicitly.""" + + address_element_3: str + """City, suburb, district, locality, or neighborhood. If omitted, derived from `address`.""" + + address_element_4: str + """State, province, region, or town. If omitted, derived from `address`.""" + + address_element_5: str + """Postcode or postal code. `postal_code` is accepted as an alias.""" + + postal_code: str + """Postal code for address-based services (alias of `address_element_5`).""" + + driver_license_number: str + """Driver licence number for government document-verification services (AUS, NZL, IND).""" + + driver_license_card_number: str + """Physical driver licence card number, where required separately from the licence number (AUS).""" + + driver_license_state: str + """Driver licence state or territory of issue (AUS).""" + + driver_license_version: str + """Driver licence version code (NZL).""" + + passport_number: str + """Passport number for government passport-verification services (AUS, NZL, IND, CHN).""" + + passport_expiration_date: str | _dt.date + """Passport expiry date, `YYYY-MM-DD`, for passport-verification services.""" + + passport_file_number: str + """Passport file number (IND).""" + + passport_issue_country: str + """Issuing country of the passport (ISO 3166-1 alpha-3) for passport-verification services.""" + + medicare_card_number: str + """Medicare card number (AUS).""" + + immi_card_number: str + """Australian ImmiCard number.""" + + immi_card_expiry_date: str | _dt.date + """Australian ImmiCard expiry date.""" + + citizenship_certificate_number: str + """Citizenship certificate number (AUS).""" + + birth_registration_number: str + """Birth-certificate registration number (AUS).""" + + birth_registration_date: str | _dt.date + """Birth-certificate registration date (AUS).""" + + birth_registration_state: str + """Birth-certificate registration state (AUS).""" + + marriage_certificate_number: str + """Marriage certificate number (AUS).""" + + change_of_name_certificate_number: str + """Change-of-name certificate number (AUS).""" + + first_partner_name: str + """First name of the partner on a marriage certificate (AUS).""" + + last_partner_name: str + """Last name of the partner on a marriage certificate (AUS).""" + + voter_id: str + """Voter registration number (IND EPIC, IRL).""" + + epic_card: str + """India EPIC voter card number.""" + + pan: str + """India PAN (Permanent Account Number).""" + + national_id: str + """National ID number (CHN, MYS, KEN, KHM, NGA, ZAF, …).""" + + cic: str + """Mexican INE/IFE Código de Identificación de Credencial (CIC), 9 digits. Used by the MEX INE credential-validity service (`mex_ine_vigencia`).""" + + identificador_ciudadano: str + """Mexican INE Identificador del Ciudadano (9 digits). Used with `cic` for modern INE models (E/F/G/H) in `mex_ine_vigencia`.""" + + ocr: str + """Mexican INE OCR number (13 digits, back of card). Used with `cic` for Model D in `mex_ine_vigencia`.""" + + voter_number: str + """Mexican INE Clave de Elector (18 chars). Used with `emission_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`.""" + + emission_number: str + """Mexican INE Número de Emisión. Used with `voter_number` for legacy IFE models (A/B/C) in `mex_ine_vigencia`.""" + + bvn: str + """Nigerian Bank Verification Number.""" + + bank_card_number: str + """Bank card number (CHN bank-card verification).""" + + ssn: str + """Social Security Number (USA).""" + + phone: str + """Phone number for phone-verification services.""" + + landline: str + """Landline number for phone-verification services.""" + + email: str + """Email address for identity-verification services.""" + + country_of_residence: str + """ISO 3166-1 alpha-2 country of residence. Used by global identity-enrichment services (e.g. `glb_identity_enrichment`) to focus the lookup.""" + + partial_match_action: Literal["DECLINE", "NO_ACTION"] | str + """What a `partial_match` does to `database_validation.status`. Defaults to `NO_ACTION` (status stays `Approved`, with a warning).""" + + no_match_action: Literal["DECLINE", "NO_ACTION"] | str + """What a `no_match` does to `database_validation.status`. Defaults to `DECLINE`.""" + + save_api_request: bool + """Persist the validation as a session (console visibility, decision endpoint, `status.updated` webhook). Also changes the `validations` response shape: per-service objects when `true` (default), a merged field map when `false`.""" + + vendor_data: str + """Your identifier for the validated user; echoed back and stored with the session.""" + + metadata: Mapping[str, object] | None + """Free-form JSON stored with the request and echoed back.""" + + selfie: FileTypes + """Selfie image (`jpg`, `jpeg`, `png`, `webp`; max 2 MB - images are compressed to ~1.5 MB before upload to the registry) for biometric validation services, e.g. Argentina RENAPER (`arg_renaper`) and Panama SIB (`pan_cedula_sib`, `pan_cedula_sib_plus`). Required whenever a selected service lists it as a required field. Only available via `multipart/form-data`.""" diff --git a/src/didit/types/database_validation/validate_response.py b/src/didit/types/database_validation/validate_response.py new file mode 100644 index 0000000..7cc3ac0 --- /dev/null +++ b/src/didit/types/database_validation/validate_response.py @@ -0,0 +1,138 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "DatabaseValidationValidateResponse", + "DatabaseValidationValidateResponseDatabaseValidation", + "DatabaseValidationValidateResponseDatabaseValidationErrors", + "DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1", + "DatabaseValidationValidateResponseDatabaseValidationWarnings", +] + + +class DatabaseValidationValidateResponseDatabaseValidationErrors(BaseModel): + service_id: str | None = None + + code: str | None = None + + message: str | None = None + + retryable: bool | None = None + """`true` when the service produced no answer and retrying it later can succeed; `false` when the source refused the data it was sent and the input must be corrected first.""" + + +class DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1(BaseModel): + """Per-service validation result (returned when `save_api_request=true`, the default).""" + + service_id: str | None = None + """Catalog id of the service that produced this result (e.g. `bra_cpf`).""" + + service_name: str | None = None + """Human-readable catalog name of the service.""" + + validation: dict[str, Literal["full_match", "partial_match", "no_match"] | str] | None = None + """Field-by-field comparison between your input and the registry record. Keys are canonical field names (`full_name`, `date_of_birth`, `identification_number`, `address`, …); values are `full_match`, `partial_match`, or `no_match`.""" + + outcome_code: ( + Literal[ + "MATCH", + "PARTIAL_MATCH", + "NO_MATCH", + "DOCUMENT_NOT_FOUND", + "INVALID_DOCUMENT_FORMAT", + "INVALID_INPUT", + "MINOR_BLOCKED", + "DECEASED", + "BIOMETRIC_NO_MATCH", + "BIOMETRIC_IMAGE_UNUSABLE", + "INCONCLUSIVE", + "DOCUMENT_SUPERSEDED", + "REGISTRY_UNAVAILABLE", + "REGISTRY_ERROR", + ] + | str + | None + ) = None + """Vendor-neutral outcome of the registry lookup. `MATCH`/`PARTIAL_MATCH`/`NO_MATCH` describe the comparison; `DOCUMENT_NOT_FOUND` means the identifier is not in the registry; `INCONCLUSIVE` means the registry could not confirm either way (routed to review - NOT a no-match); `BIOMETRIC_NO_MATCH` is a definitive face mismatch while `BIOMETRIC_IMAGE_UNUSABLE` is a technical image problem (retake the selfie); `DECEASED`/`MINOR_BLOCKED` are registry-policy outcomes; `DOCUMENT_SUPERSEDED` means the identity matched but the edition of the document presented is not the one on file (a newer edition has been issued); `INVALID_DOCUMENT_FORMAT`/`INVALID_INPUT` mean the registry itself rejected the data it was sent; `REGISTRY_UNAVAILABLE`/`REGISTRY_ERROR` indicate upstream failures.""" + + outcome_detail: str | None = None + """Raw upstream status detail backing the outcome code (e.g. an HTTP or registry status code).""" + + source_data: dict[str, object] | None = None + """Cleaned, normalized view of the registry record using canonical field names (`identification_number`, `first_name`, `last_name`, `date_of_birth`, plus registry-specific flags). Image fields, when present, are returned as pre-signed URLs.""" + + +class DatabaseValidationValidateResponseDatabaseValidationWarnings(BaseModel): + feature: str | None = None + """Feature that raised the warning.""" + + risk: str | None = None + """Machine-readable risk identifier.""" + + additional_data: dict[str, object] | None = None + """Extra context for the warning, when available.""" + + log_type: str | None = None + """`warning`, `information`, or `error` - how the risk affected the outcome.""" + + short_description: str | None = None + """One-line human-readable summary.""" + + long_description: str | None = None + """Full human-readable explanation.""" + + +class DatabaseValidationValidateResponseDatabaseValidation(BaseModel): + status: Literal["Approved", "Declined", "In Review"] | str | None = None + """`Declined` when the overall `match_type` triggers a `DECLINE` action (`no_match_action` defaults to `DECLINE`, `partial_match_action` to `NO_ACTION`); `In Review` when no service produced a usable comparison, for example when the only selected service came back `REGISTRY_UNAVAILABLE` or `INCONCLUSIVE`; `Approved` otherwise. A request that selected several services can be `Approved` on one service's match while another was unavailable, so check `errors` and the per-service `outcome_code`s too.""" + + issuing_state: str | None = None + """Echo of the ISO 3166-1 alpha-3 country validated against.""" + + validation_type: Literal["one_by_one", "two_by_two"] | str | None = None + """Derived from the results: `two_by_two` when ≥2 distinct services produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions); `one_by_one` otherwise. (The request field of the same name is deprecated and ignored.)""" + + screened_data: dict[str, object] | None = None + """Echo of the input fields that were sent to the selected services (dates normalized to `YYYY-MM-DD`; a `selfie`, when sent, is returned as a pre-signed URL; `consent_obtained: true` is added when consent was given).""" + + match_type: Literal["full_match", "partial_match", "no_match"] | str | None = None + """Overall result aggregated across services with field-level comparisons: any `full_match` → `full_match`; else any `partial_match` → `partial_match`; else `no_match`. Outcome-only services, such as biometric criminal screening, can return `null`; inspect each item in `validations` for its `outcome_code`.""" + + validations: ( + list[DatabaseValidationValidateResponseDatabaseValidationValidationsVariant1] + | dict[str, Literal["full_match", "partial_match", "no_match"] | str] + | None + ) = None + """Shape depends on `save_api_request`. **`true` (default):** an array of per-service objects (`service_id`, `service_name`, `validation`, `outcome_code`, `outcome_detail`, `source_data`). **`false`:** a single flat object merging the field-level results of all services (`{"full_name": "full_match", ...}`).""" + + warnings: list[DatabaseValidationValidateResponseDatabaseValidationWarnings] | None = None + """`DATABASE_VALIDATION_NO_MATCH` or `DATABASE_VALIDATION_PARTIAL_MATCH` when applicable; empty on a full match.""" + + errors: list[DatabaseValidationValidateResponseDatabaseValidationErrors] | None = None + """Present only when some (but not all) selected services failed. Each entry carries `service_id`, `code`, `message`, and `retryable`. A service reported here is absent from `validations` and `services_used`, and is not billed. This array is the only place it appears, so inspect it even when `status` is `Approved`.""" + + services_used: list[str] | None = None + """Catalog ids of the services that returned a billable result. **Only present when the request explicitly sent `services`.**""" + + match_score: float | None = None + """Fraction (0.0–1.0) of attempted services that produced a full match (on the identification number, on full name + date of birth together, or on the address without contradictions). **Only present when the request explicitly sent `services`.**""" + + +class DatabaseValidationValidateResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + database_validation: DatabaseValidationValidateResponseDatabaseValidation | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/document_ai/__init__.py b/src/didit/types/document_ai/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/document_ai/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/document_ai/analyze_params.py b/src/didit/types/document_ai/analyze_params.py new file mode 100644 index 0000000..78ba1f6 --- /dev/null +++ b/src/didit/types/document_ai/analyze_params.py @@ -0,0 +1,57 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["DocumentAiAnalyzeParams"] + + +class DocumentAiAnalyzeParams(TypedDict, total=False): + document: Required[FileTypes] + """The document to read. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`, `zip`. Maximum upload size: **30 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs are rejected with `400`. Images are automatically compressed before processing.""" + + fields: Required[str] + """One JSON-encoded array string with **1–30** field definitions (maximum 16 KB). Each item: `key` (required, `^[a-z0-9_]{1,64}$`, unique - the key returned in `extracted_data`), `name` (≤100 chars, shown in messages), `instruction` (≤500 chars, what the model should read), `type` (`text` default | `number` | `date`), `required` (boolean, default `false`), `is_full_name` (boolean, default `false`, at most one per request). Bracket-style form arrays, invalid JSON, duplicate keys, or more than one `is_full_name` return `400` before any processing or billing.""" + + expected_first_name: str + """First name to verify against the document's name field (fuzzy match, transliteration-aware). Use together with `expected_last_name`; cannot be combined with `expected_company_name`.""" + + expected_last_name: str + """Last name to verify against the document's name field, combined with `expected_first_name` for `name_match_score`.""" + + expected_company_name: str + """Company name to verify against the document's name field (for business documents). Cannot be combined with `expected_first_name` / `expected_last_name` (no example value on purpose - the generated request sample uses the person fields; swap them for this one when verifying a business document).""" + + expected_company_country: str + """Optional ISO 3166-1 alpha-2 country code (two ASCII letters, e.g. `FR`) that tunes company-name normalization for `expected_company_name`. Only used with `expected_company_name`.""" + + document_ai_name_match_score_threshold: int + """Minimum `name_match_score` (0–100) to consider the name matched. A lower score adds `DOCUMENT_AI_NAME_MISMATCH`, governed by `document_ai_name_mismatch_action`.""" + + document_ai_unreadable_document_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] | str + """What to do when no field could be read from the document (`DOCUMENT_AI_UNREADABLE_DOCUMENT`).""" + + document_ai_missing_required_fields_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] | str + """What to do when at least one `required` field could not be read (`DOCUMENT_AI_MISSING_REQUIRED_FIELDS`; `additional_data.missing_fields` lists them).""" + + document_ai_document_tampering_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] | str + """What to do when PDF/EXIF forensics suspect manipulation - modified after digital signing, known PDF editor, overlay-text manipulation, suspicious re-export, image anomalies (`DOCUMENT_AI_SUSPECTED_MANIPULATION`).""" + + document_ai_name_mismatch_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] | str + """What to do when `name_match_score` is below the threshold (`DOCUMENT_AI_NAME_MISMATCH`).""" + + document_ai_unsupported_file_action: Literal["DECLINE", "REVIEW", "NO_ACTION"] | str + """What to do when the upload is empty or not a processable file (`DOCUMENT_AI_UNSUPPORTED_FILE`).""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, with the uploaded document, the extracted values, the field definitions and the name-match snapshot stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only; the call is still billed.""" + + vendor_data: str + """Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object.""" diff --git a/src/didit/types/document_ai/analyze_response.py b/src/didit/types/document_ai/analyze_response.py new file mode 100644 index 0000000..1c9ea63 --- /dev/null +++ b/src/didit/types/document_ai/analyze_response.py @@ -0,0 +1,98 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.document_ai_detected_code import DocumentAIDetectedCode + +__all__ = [ + "DocumentAiAnalyzeResponse", + "DocumentAiAnalyzeResponseDocumentAi", + "DocumentAiAnalyzeResponseDocumentAiFields", + "DocumentAiAnalyzeResponseDocumentAiWarnings", +] + + +class DocumentAiAnalyzeResponseDocumentAiFields(BaseModel): + key: str | None = None + + name: str | None = None + + instruction: str | None = None + + type: Literal["text", "number", "date"] | str | None = None + + required: bool | None = None + + is_full_name: bool | None = None + + +class DocumentAiAnalyzeResponseDocumentAiWarnings(BaseModel): + risk: ( + Literal[ + "DOCUMENT_AI_UNREADABLE_DOCUMENT", + "DOCUMENT_AI_MISSING_REQUIRED_FIELDS", + "DOCUMENT_AI_SUSPECTED_MANIPULATION", + "DOCUMENT_AI_NAME_MISMATCH", + "DOCUMENT_AI_UNSUPPORTED_FILE", + ] + | str + | None + ) = None + """Machine-readable risk code. Each one is governed by the matching `document_ai_*_action` request option.""" + + feature: Literal["DOCUMENT_AI"] | str | None = None + """Feature that raised the warning. Always `DOCUMENT_AI` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """Extra context for the risk: `{"missing_fields": ["Net pay"]}` for missing required fields, `{"name_on_document", "score", "threshold"}` for a name mismatch, and the forensic finding (`detection_method`, producer/editor details) for suspected manipulation. `null` when there is nothing to add.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity derived from the configured action: `error` (DECLINE), `warning` (REVIEW), `information` (NO_ACTION).""" + + short_description: str | None = None + """Human-readable one-liner for the risk.""" + + long_description: str | None = None + """Human-readable explanation, interpolated with `additional_data` where relevant.""" + + +class DocumentAiAnalyzeResponseDocumentAi(BaseModel): + status: Literal["Approved", "Declined", "In Review"] | str | None = None + """`Approved` when no warning resolves to a decline or review; `Declined` when at least one risk maps to `DECLINE`; otherwise `In Review` when at least one maps to `REVIEW`. The `warnings` list explains why.""" + + extracted_data: dict[str, object] | None = None + """One entry per requested field key. Values are typed per the field `type` - strings for `text`, numbers for `number`, `YYYY-MM-DD` strings for `date` - or `null` when the value could not be read.""" + + detected_codes: list[DocumentAIDetectedCode] | None = None + """QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.""" + + fields: list[DocumentAiAnalyzeResponseDocumentAiFields] | None = None + """The normalized field definitions that were used for this extraction, echoed back (also stored with the session when `save_api_request=true`).""" + + name_match_score: float | None = None + """Fuzzy similarity (0–100) between `name_on_document` and the expected person/company name. Returned whenever both a name value and an expected name exist - on matches **and** mismatches; `null` otherwise.""" + + name_on_document: str | None = None + """The name value taken from the document (the `is_full_name` field, or the first name-like text field) that was compared against the expected name. `null` when no name value was available.""" + + warnings: list[DocumentAiAnalyzeResponseDocumentAiWarnings] | None = None + """Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`, entries with `log_type: "warning"` set it to `In Review`.""" + + +class DocumentAiAnalyzeResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + document_ai: DocumentAiAnalyzeResponseDocumentAi | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """When the request was processed (UTC, ISO 8601).""" diff --git a/src/didit/types/email/__init__.py b/src/didit/types/email/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/email/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/email/assess_risk_params.py b/src/didit/types/email/assess_risk_params.py new file mode 100644 index 0000000..015d23a --- /dev/null +++ b/src/didit/types/email/assess_risk_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.risk_signals import RiskSignals + +__all__ = ["EmailAssessRiskParams"] + + +class EmailAssessRiskParams(TypedDict, total=False): + email: Required[str] + """Email address to score.""" + + country_code: str + """Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage.""" + + signals: RiskSignals + """Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks.""" + + vendor_data: str + """Your stable reference for this request. Stored on the API session and echoed in the response.""" + + metadata: Mapping[str, object] | None + """Your non-sensitive JSON metadata. Stored on the API session and echoed in the response.""" + + enable_social: bool + """When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price.""" diff --git a/src/didit/types/email/check_code_params.py b/src/didit/types/email/check_code_params.py new file mode 100644 index 0000000..117b0a7 --- /dev/null +++ b/src/didit/types/email/check_code_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["EmailCheckCodeParams"] + + +class EmailCheckCodeParams(TypedDict, total=False): + email: Required[str] + """The same email address used in the matching [`POST /v3/email/send/`](/standalone-apis/email-send) call. This is what links the check to the send.""" + + code: Required[str] + """The OTP the end user received: 4–8 digits, or 4–8 letters/digits when `alphanumeric_code: true` was used at send time. Comparison is case-insensitive.""" + + duplicated_email_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the same address was already used and approved by a different user (only previously Approved verifications count) (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings` and fills `email.matches`.""" + + breached_email_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the address appears in known data breaches (`email.is_breached`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`.""" + + disposable_email_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the domain belongs to a disposable/temporary-mail provider (`email.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `email.warnings`.""" + + enable_social: bool + """When true, also look up the social footprint of the address - the email, social, professional, e-commerce and entertainment platforms it is registered on. Billed as one `email_social` unit per request in addition to this endpoint's own price.""" + + email_no_social_presence_action: Literal["NO_ACTION", "DECLINE"] | str + """Action when the social footprint check finds the address registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name.""" diff --git a/src/didit/types/email/send_code_params.py b/src/didit/types/email/send_code_params.py new file mode 100644 index 0000000..d151cd0 --- /dev/null +++ b/src/didit/types/email/send_code_params.py @@ -0,0 +1,124 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["EmailSendCodeParams", "EmailSendCodeParamsOptions", "EmailSendCodeParamsSignals"] + + +class EmailSendCodeParamsOptions(TypedDict, total=False): + """OTP format, localization, and branding options. All fields are optional.""" + + code_size: int + """Number of characters in the OTP (digits by default; uppercase letters and digits when `alphanumeric_code` is `true`).""" + + alphanumeric_code: bool + """When `true`, the OTP is composed of uppercase letters `A–Z` and digits `0–9` instead of digits only. The `/v3/email/check/` comparison is case-insensitive.""" + + locale: ( + Literal[ + "en", + "ar", + "bn", + "bg", + "bs", + "ca", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "cnr", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh", + ] + | str + ) + """Language of the OTP email template. Defaults to `en`. Unsupported values return `400` listing all supported locales.""" + + use_white_label_customization: bool + """When `true`, the OTP email uses your application's white-label customization (logo, colors, sender branding) instead of the default Didit template.""" + + +class EmailSendCodeParamsSignals(TypedDict, total=False): + """Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.""" + + ip: str + """IP address of the end user's device. IPv4 or IPv6.""" + + device_id: str + """Unique identifier of the end user's device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.""" + + device_platform: Literal["android", "ios", "ipados", "tvos", "web"] | str + """Platform of the end user's device.""" + + device_model: str + """Model of the end user's device.""" + + os_version: str + """Operating-system version of the end user's device.""" + + app_version: str + """Version of your application.""" + + user_agent: str + """User agent of the end user's browser or app.""" + + +class EmailSendCodeParams(TypedDict, total=False): + email: Required[str] + """Recipient email address. Malformed addresses return `400`; syntactically valid addresses that cannot receive mail (failed DNS/MX validation) return `200` with `status: "Undeliverable"`.""" + + options: EmailSendCodeParamsOptions + """OTP format, localization, and branding options. All fields are optional.""" + + signals: EmailSendCodeParamsSignals + """Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.""" + + vendor_data: str + """Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record.""" + + metadata: Mapping[str, object] | None + """Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console.""" diff --git a/src/didit/types/face_match/__init__.py b/src/didit/types/face_match/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/face_match/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/face_match/compare_params.py b/src/didit/types/face_match/compare_params.py new file mode 100644 index 0000000..b0c1003 --- /dev/null +++ b/src/didit/types/face_match/compare_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["FaceMatchCompareParams"] + + +class FaceMatchCompareParams(TypedDict, total=False): + user_image: Required[FileTypes] + """Live or candidate face image to verify (e.g. a selfie). Submit a single front-facing photo with the subject's face clearly visible and well lit. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy.""" + + ref_image: Required[FileTypes] + """Reference face image to compare against (e.g. an ID document portrait or a previously enrolled photo). Same format and size limits as `user_image`.""" + + face_match_score_decline_threshold: float + """Similarity threshold (0–100) that drives the response `status`. A computed score **at or below** this value sets `status` to `Declined` and adds a `LOW_FACE_MATCH_SIMILARITY` warning; a score strictly above it returns `Approved`. Default `30` is permissive - raise it (e.g. `50`–`60`) for stricter verification. Values outside 0–100 return `400`.""" + + rotate_image: bool + """When `true`, the service tries the input images in 90-degree increments (0, 90, 180, 270) and uses the orientation that yields the best face detection - useful for mobile captures with missing EXIF orientation. Adds latency, so only enable it if you cannot guarantee upright images.""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, and announced through a `status.updated` webhook. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.""" + + vendor_data: str + """Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"login"}`) - it is parsed into an object.""" diff --git a/src/didit/types/face_match/compare_response.py b/src/didit/types/face_match/compare_response.py new file mode 100644 index 0000000..6bb1627 --- /dev/null +++ b/src/didit/types/face_match/compare_response.py @@ -0,0 +1,123 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "FaceMatchCompareResponse", + "FaceMatchCompareResponseFaceMatch", + "FaceMatchCompareResponseFaceMatchRefImage", + "FaceMatchCompareResponseFaceMatchRefImageEntities", + "FaceMatchCompareResponseFaceMatchUserImage", + "FaceMatchCompareResponseFaceMatchUserImageEntities", + "FaceMatchCompareResponseFaceMatchWarnings", +] + + +class FaceMatchCompareResponseFaceMatchRefImageEntities(BaseModel): + bbox: list[int] | None = None + """Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.""" + + confidence: float | None = None + """Face-detection confidence (0–1).""" + + age: float | None = None + """Model-estimated age of the detected face, in years. Informational only - it does not affect the face-match decision.""" + + gender: str | None = None + """Model-predicted gender of the detected face (`male` or `female`). Informational only.""" + + race: str | None = None + """Reserved field - always `null` in responses from this endpoint.""" + + +class FaceMatchCompareResponseFaceMatchRefImage(BaseModel): + """Face-detection results for the reference image, same shape as `user_image`.""" + + entities: list[FaceMatchCompareResponseFaceMatchRefImageEntities] | None = None + """One entry per detected face.""" + + best_angle: int | None = None + + +class FaceMatchCompareResponseFaceMatchUserImageEntities(BaseModel): + bbox: list[int] | None = None + """Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.""" + + confidence: float | None = None + """Face-detection confidence (0–1).""" + + age: float | None = None + """Model-estimated age of the detected face, in years. Informational only - it does not affect the face-match decision.""" + + gender: str | None = None + """Model-predicted gender of the detected face (`male` or `female`). Informational only.""" + + race: str | None = None + """Reserved field - always `null` in responses from this endpoint.""" + + +class FaceMatchCompareResponseFaceMatchUserImage(BaseModel): + """Face-detection results for the analyzed images.""" + + entities: list[FaceMatchCompareResponseFaceMatchUserImageEntities] | None = None + """One entry per detected face.""" + + best_angle: int | None = None + """Rotation (degrees: 0, 90, 180, or 270) that produced the best face detection. Only non-zero when `rotate_image=true` corrected the orientation.""" + + +class FaceMatchCompareResponseFaceMatchWarnings(BaseModel): + risk: Literal["NO_REFERENCE_IMAGE", "LOW_FACE_MATCH_SIMILARITY"] | str | None = None + """Machine-readable risk code.""" + + feature: Literal["FACEMATCH"] | str | None = None + """Feature that raised the warning. Always `FACEMATCH` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """Always `null` for face-match warnings.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own.""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class FaceMatchCompareResponseFaceMatch(BaseModel): + status: Literal["Approved", "Declined"] | str | None = None + """`Approved` when the similarity score is strictly above `face_match_score_decline_threshold` and a score could be computed; `Declined` otherwise (a warning explains why).""" + + score: float | None = None + """Similarity score from 0 to 100 - the model's confidence that both images depict the same person. Rounded to two decimals only when `save_api_request` is true; with `save_api_request=false` the raw float is returned. `null` when no face pair could be scored (with the default `save_api_request=true`); with `save_api_request=false` the same condition can surface as `score: 0`. Either way a `NO_REFERENCE_IMAGE` warning is present and `status` is `Declined`.""" + + user_image: FaceMatchCompareResponseFaceMatchUserImage | None = None + """Face-detection results for the analyzed images.""" + + ref_image: FaceMatchCompareResponseFaceMatchRefImage | None = None + """Face-detection results for the reference image, same shape as `user_image`.""" + + warnings: list[FaceMatchCompareResponseFaceMatchWarnings] | None = None + """Empty on a clean approval. `NO_REFERENCE_IMAGE` (`error`) - no face pair could be scored; `LOW_FACE_MATCH_SIMILARITY` (`error`) - score at or below the decline threshold. Any of these sets `status` to `Declined`.""" + + +class FaceMatchCompareResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + face_match: FaceMatchCompareResponseFaceMatch | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`.""" diff --git a/src/didit/types/face_search/__init__.py b/src/didit/types/face_search/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/face_search/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/face_search/search_params.py b/src/didit/types/face_search/search_params.py new file mode 100644 index 0000000..3869d77 --- /dev/null +++ b/src/didit/types/face_search/search_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["FaceSearchSearchParams"] + + +class FaceSearchSearchParams(TypedDict, total=False): + user_image: Required[FileTypes] + """Front-facing face image to search with. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image must contain at least one detectable face - otherwise the endpoint returns `400`. When several faces are present, the largest one is searched and a `MULTIPLE_FACES_DETECTED` warning is added.""" + + search_type: Literal["most_similar", "blocklisted_or_approved"] | str + """Search policy. `most_similar` (default) ranks every enrolled face in your application by similarity. `blocklisted_or_approved` restricts candidates to blocklisted faces, allowlisted faces, faces from approved sessions, and imported user-profile faces - with blocklisted entries ranked first.""" + + rotate_image: bool + """When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency.""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session, emits a `status.updated` webhook, presigns `match_image_url` values, and enrolls the searched face into your face search index (Face Search enrollments are excluded from future searches). When `false`, the search is one-shot - no session is stored and the face is not enrolled.""" + + vendor_data: str + """Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. It does not exclude same-user faces from the search results.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field - it is parsed into an object.""" diff --git a/src/didit/types/face_search/search_response.py b/src/didit/types/face_search/search_response.py new file mode 100644 index 0000000..0537209 --- /dev/null +++ b/src/didit/types/face_search/search_response.py @@ -0,0 +1,152 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "FaceSearchSearchResponse", + "FaceSearchSearchResponseFaceSearch", + "FaceSearchSearchResponseFaceSearchMatches", + "FaceSearchSearchResponseFaceSearchMatchesUserDetails", + "FaceSearchSearchResponseFaceSearchUserImage", + "FaceSearchSearchResponseFaceSearchUserImageEntities", + "FaceSearchSearchResponseFaceSearchWarnings", +] + + +class FaceSearchSearchResponseFaceSearchMatchesUserDetails(BaseModel): + """Identity data extracted during the matched session's document verification. `null` when the matched session has no document data (e.g. liveness-only sessions) and always `null` for `retained_template` matches. For `source: imported` matches it is populated from the User profile instead: `full_name` set, `document_type`/`document_number` null.""" + + full_name: str | None = None + + document_type: str | None = None + + document_number: str | None = None + + +class FaceSearchSearchResponseFaceSearchMatches(BaseModel): + session_id: str | None = None + """Id of the verification session the matched face belongs to. `null` when `source` is `imported`, `list_entry`, or `retained_template`.""" + + session_number: int | None = None + """Human-friendly session number shown in the Business Console. `null` for non-session matches.""" + + similarity_percentage: float | None = None + """Similarity between the submitted face and the matched face (0–100).""" + + source: Literal["session", "imported", "list_entry", "retained_template"] | str | None = None + """Where the matched face was enrolled from: `session` - a verification session or a saved standalone API call; `imported` - a face uploaded directly to a User profile; `list_entry` - a face attached to one of your lists; `retained_template` - an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`.""" + + vendor_data: str | None = None + """`vendor_data` of the matched session or linked User for imported and retained-template faces.""" + + vendor_user_id: str | None = None + """Linked User id. `null` for list entries and session matches without a User.""" + + biometric_template_id: str | None = None + """Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise.""" + + verification_date: _dt.datetime | None = None + """When the matched face was originally captured, or when a retained template was created (`YYYY-MM-DDThh:mm:ssZ`). `null` for `list_entry` matches.""" + + user_details: FaceSearchSearchResponseFaceSearchMatchesUserDetails | None = None + """Identity data extracted during the matched session's document verification. `null` when the matched session has no document data (e.g. liveness-only sessions) and always `null` for `retained_template` matches. For `source: imported` matches it is populated from the User profile instead: `full_name` set, `document_type`/`document_number` null.""" + + match_image_url: str | None = None + """Time-limited URL (`https:///...`) of the matched face's reference image when `save_api_request=true`. With `save_api_request=false` this is an internal storage path that is not directly downloadable. Always `null` for `retained_template` matches because retained templates contain no image.""" + + status: str | None = None + """Status of the matched session (`Approved`, `Declined`, `In Review`, …). `null` for `imported`/`list_entry` matches.""" + + is_blocklisted: bool | None = None + """`true` when the matched face is on your face blocklist. Any sufficiently similar blocklisted match declines the search.""" + + is_allowlisted: bool | None = None + """`true` when the matched face is on one of your allowlists. Allowlisted matches suppress duplicate warnings.""" + + api_service: str | None = None + """Set when the matched face was enrolled by a standalone API call (e.g. `PASSIVE_LIVENESS`); `null` for regular verification sessions and imported faces.""" + + +class FaceSearchSearchResponseFaceSearchUserImageEntities(BaseModel): + bbox: list[int] | None = None + """Bounding box `[x_min, y_min, x_max, y_max]` in pixels.""" + + confidence: float | None = None + """Face-detection confidence (0–1).""" + + +class FaceSearchSearchResponseFaceSearchUserImage(BaseModel): + """Face-detection results for the submitted image. Entries here carry only `bbox` and `confidence`.""" + + entities: list[FaceSearchSearchResponseFaceSearchUserImageEntities] | None = None + """One entry per detected face. The largest face is the one searched.""" + + best_angle: int | None = None + """Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation. Never null on this endpoint (defaults to 0 when the model omits it).""" + + +class FaceSearchSearchResponseFaceSearchWarnings(BaseModel): + risk: ( + Literal[ + "MULTIPLE_FACES_DETECTED", + "FACE_IN_BLOCKLIST", + "POSSIBLE_FACE_IN_BLOCKLIST", + "DUPLICATED_FACE", + "POSSIBLE_DUPLICATED_FACE", + ] + | str + | None + ) = None + """Machine-readable risk code.""" + + feature: Literal["LIVENESS"] | str | None = None + """Feature that raised the warning. Always `LIVENESS` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """`null` for `MULTIPLE_FACES_DETECTED`. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the first matching session.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own.""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class FaceSearchSearchResponseFaceSearch(BaseModel): + status: Literal["Approved", "Declined"] | str | None = None + """`Declined` only when a `FACE_IN_BLOCKLIST` or `POSSIBLE_FACE_IN_BLOCKLIST` warning fired. Duplicate matches and multiple-face warnings never decline.""" + + total_matches: int | None = None + """Number of entries in `matches` (0–5).""" + + matches: list[FaceSearchSearchResponseFaceSearchMatches] | None = None + """Up to 5 faces from your face search index that exceed the similarity floor, ordered by similarity (with blocklisted entries ranked first when `search_type=blocklisted_or_approved`). Empty when no enrolled face is similar enough.""" + + user_image: FaceSearchSearchResponseFaceSearchUserImage | None = None + """Face-detection results for the submitted image. Entries here carry only `bbox` and `confidence`.""" + + warnings: list[FaceSearchSearchResponseFaceSearchWarnings] | None = None + """Risk signals. `FACE_IN_BLOCKLIST` / `POSSIBLE_FACE_IN_BLOCKLIST` (`error`) decline the search; `MULTIPLE_FACES_DETECTED` (`warning`) and `DUPLICATED_FACE` / `POSSIBLE_DUPLICATED_FACE` (`information`) are advisory.""" + + +class FaceSearchSearchResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + face_search: FaceSearchSearchResponseFaceSearch | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`.""" diff --git a/src/didit/types/fraud_checks/__init__.py b/src/didit/types/fraud_checks/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/fraud_checks/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/fraud_checks/create_params.py b/src/didit/types/fraud_checks/create_params.py new file mode 100644 index 0000000..394b2ac --- /dev/null +++ b/src/didit/types/fraud_checks/create_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit.types.components.fraud_check_context_request import FraudCheckContextRequest +from didit.types.components.fraud_check_module_selection_request import ( + FraudCheckModuleSelectionRequest, +) +from didit.types.components.fraud_check_subject_request import FraudCheckSubjectRequest + +__all__ = ["FraudChecksCreateParams"] + + +class FraudChecksCreateParams(TypedDict, total=False): + client_reference: str + """Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409.""" + + profile: str + """Orchestration profile id. Defaults to the Didit `standard` profile.""" + + profile_version: str + """Pin a frozen profile version. Defaults to the current version of the profile.""" + + subject: Required[FraudCheckSubjectRequest] + + context: FraudCheckContextRequest + + checks: FraudCheckModuleSelectionRequest + + vendor_data: str + + metadata: object diff --git a/src/didit/types/fraud_checks/plan_params.py b/src/didit/types/fraud_checks/plan_params.py new file mode 100644 index 0000000..fe4c4a9 --- /dev/null +++ b/src/didit/types/fraud_checks/plan_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +from didit.types.components.fraud_check_context_request import FraudCheckContextRequest +from didit.types.components.fraud_check_module_selection_request import ( + FraudCheckModuleSelectionRequest, +) +from didit.types.components.fraud_check_subject_request import FraudCheckSubjectRequest + +__all__ = ["FraudChecksPlanParams"] + + +class FraudChecksPlanParams(TypedDict, total=False): + client_reference: str + """Your own reference for this check. Doubles as the idempotency key: reusing it with the same body returns the original check without running or billing the modules again; reusing it with a different body returns 409.""" + + profile: str + """Orchestration profile id. Defaults to the Didit `standard` profile.""" + + profile_version: str + """Pin a frozen profile version. Defaults to the current version of the profile.""" + + subject: Required[FraudCheckSubjectRequest] + + context: FraudCheckContextRequest + + checks: FraudCheckModuleSelectionRequest + + vendor_data: str + + metadata: object diff --git a/src/didit/types/id_verification/__init__.py b/src/didit/types/id_verification/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/id_verification/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/id_verification/verify_params.py b/src/didit/types/id_verification/verify_params.py new file mode 100644 index 0000000..fe4b067 --- /dev/null +++ b/src/didit/types/id_verification/verify_params.py @@ -0,0 +1,57 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["IdVerificationVerifyParams"] + + +class IdVerificationVerifyParams(TypedDict, total=False): + front_image: Required[FileTypes] + """Front side of the identity document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`, `pdf`. Maximum upload size: **10 MB** (larger files are rejected with `400`). PDFs are rendered to an image before OCR; encrypted PDFs require `front_image_password`. Images are automatically compressed to ~0.5 MB and EXIF orientation is applied. Upload the full, uncropped capture with all four corners of the document visible.""" + + front_image_password: str + """Password to decrypt `front_image` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`.""" + + back_image: FileTypes + """Back side of the document - send it whenever the document has one (many ID cards carry the MRZ or a barcode on the back; omitting it simply leaves the MRZ and back-side fields empty in the response - this endpoint does not raise a missing-MRZ warning). Same format and size limits as `front_image`. When omitted, `back_image` and back-side fields are `null` in the response.""" + + back_image_password: str + """Password to decrypt `back_image` when it is an encrypted PDF.""" + + perform_document_liveness: bool + """Screens both images for presentation fraud: screen replays (`SCREEN_CAPTURE_DETECTED`), printed copies (`PRINTED_COPY_DETECTED`), and portrait manipulation (`PORTRAIT_MANIPULATION_DETECTED`). Any of these auto-declines. Enabled by default; set to `false` to skip fraud screening and reduce latency.""" + + minimum_age: int | None + """Accepted and validated (1–120) but **currently not applied** by this endpoint - it never produces a `MINIMUM_AGE_NOT_MET` warning from this field. Enforce age rules from the returned `date_of_birth`/`age`, or use a verification-session workflow with age restrictions.""" + + expiration_date_not_detected_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when no expiration date can be read (`EXPIRATION_DATE_NOT_DETECTED`). Default `NO_ACTION` because many identity documents print no expiry date. Note: a *detected and past* expiry date always declines (`DOCUMENT_EXPIRED`), regardless of this option.""" + + invalid_mrz_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the extracted MRZ fails check-digit validation (`MRZ_VALIDATION_FAILED`). A missing MRZ raises no warning on this endpoint. Only relevant for documents that carry an MRZ. `REVIEW` is not supported on this endpoint and returns `400`.""" + + inconsistent_data_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when extracted data is internally inconsistent: `DATA_INCONSISTENT`, `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` (visual zone disagrees with the MRZ), or `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS`.""" + + critical_field_occlusion_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when a critical printed field was physically covered on the capture (`CRITICAL_FIELD_OCCLUSION_DETECTED`) - an identity number, a name, the date of birth, or the expiry date. Declines by default, like this endpoint's other document-integrity risks: covering the numbers a report is reconciled against is what a redacted document looks like. The value read for a covered field is dropped from the extraction either way, so the matching `*_NOT_DETECTED` warning usually accompanies it. `REVIEW` is not supported on this endpoint and returns `400`.""" + + document_or_personal_number_format_mismatch_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when a number does not match the format its issuing authority uses for that document type (`DOCUMENT_NUMBER_FORMAT_MISMATCH`, `PERSONAL_NUMBER_FORMAT_MISMATCH`). Default `NO_ACTION`, which reports the warning without changing the status. `REVIEW` is not supported on this endpoint and returns `400`.""" + + preferred_characters: Literal["latin", "non_latin"] | str + """Preferred script for name/address fields on documents that print both Latin and non-Latin text (Arabic, Cyrillic, CJK, …). `latin` returns transliterated/Latin values; `non_latin` prefers the native script.""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with document images returned as media URLs. When `false`, nothing is stored, `request_id` is a transient UUID, and images are returned inline as base64 JPEG strings.""" + + vendor_data: str + """Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object.""" diff --git a/src/didit/types/id_verification/verify_response.py b/src/didit/types/id_verification/verify_response.py new file mode 100644 index 0000000..e392b0b --- /dev/null +++ b/src/didit/types/id_verification/verify_response.py @@ -0,0 +1,322 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "IdVerificationVerifyResponse", + "IdVerificationVerifyResponseIdVerification", + "IdVerificationVerifyResponseIdVerificationBackImageQualityScore", + "IdVerificationVerifyResponseIdVerificationBarcodes", + "IdVerificationVerifyResponseIdVerificationFrontImageQualityScore", + "IdVerificationVerifyResponseIdVerificationMrz", + "IdVerificationVerifyResponseIdVerificationParsedAddress", + "IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation", + "IdVerificationVerifyResponseIdVerificationWarnings", +] + + +class IdVerificationVerifyResponseIdVerificationBackImageQualityScore(BaseModel): + """Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.""" + + focus_score: float | None = None + """Sharpness of the document crop (higher is sharper).""" + + brightness_score: float | None = None + """Exposure quality (higher is better).""" + + brightness_issue: str | None = None + """`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright).""" + + is_document_fully_visible: bool | None = None + """Whether all four corners of the document are inside the frame.""" + + resolution_score: float | None = None + """Effective resolution of the document area.""" + + overall_score: float | None = None + """Aggregate quality score.""" + + +class IdVerificationVerifyResponseIdVerificationBarcodes(BaseModel): + type: str | None = None + """Barcode type (e.g. `PDF417`, `QR_CODE`, `UNKNOWN`).""" + + position: dict[str, object] | None = None + """Location of the barcode in the image, when available.""" + + data: str | None = None + """Decoded payload.""" + + data_raw: str | None = None + """Raw (undecoded) payload, when different.""" + + side: str | None = None + """`front` or `back`.""" + + +class IdVerificationVerifyResponseIdVerificationFrontImageQualityScore(BaseModel): + """Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.""" + + focus_score: float | None = None + """Sharpness of the document crop (higher is sharper).""" + + brightness_score: float | None = None + """Exposure quality (higher is better).""" + + brightness_issue: str | None = None + """`ok` when exposure is fine, otherwise the detected issue (e.g. too dark / too bright).""" + + is_document_fully_visible: bool | None = None + """Whether all four corners of the document are inside the frame.""" + + resolution_score: float | None = None + """Effective resolution of the document area.""" + + overall_score: float | None = None + """Aggregate quality score.""" + + +class IdVerificationVerifyResponseIdVerificationMrz(BaseModel): + """Parsed machine-readable zone. An **empty object** (`{}`) when the document has no MRZ or it could not be read - also check the `warnings` list in that case. Dates inside the MRZ use the raw `YYMMDD` format.""" + + surname: str | None = None + + name: str | None = None + + country: str | None = None + """Issuing country (ISO 3166-1 alpha-3).""" + + nationality: str | None = None + """Holder nationality (ISO 3166-1 alpha-3).""" + + birth_date: str | None = None + """Raw MRZ date of birth (`YYMMDD`).""" + + expiry_date: str | None = None + """Raw MRZ expiry date (`YYMMDD`).""" + + sex: str | None = None + + document_type: str | None = None + """MRZ document-type code (e.g. `ID`, `P`).""" + + document_number: str | None = None + + optional_data: str | None = None + + optional_data_2: str | None = None + + birth_date_hash: str | None = None + """MRZ check digit for the birth date.""" + + expiry_date_hash: str | None = None + """MRZ check digit for the expiry date.""" + + document_number_hash: str | None = None + """MRZ check digit for the document number.""" + + final_hash: str | None = None + """Composite MRZ check digit.""" + + personal_number: str | None = None + + warnings: list[str] | None = None + """Non-fatal MRZ parsing warnings.""" + + errors: list[str] | None = None + """MRZ check-digit/parsing errors. Non-empty errors surface as an `MRZ_VALIDATION_FAILED` warning.""" + + mrz_type: str | None = None + """MRZ layout: `TD1` (ID cards, 3 lines), `TD2`, or `TD3` (passports, 2 lines).""" + + mrz_string: str | None = None + """Raw MRZ text as read, lines separated by `\\n`.""" + + mrz_key: str | None = None + """Composite key (document number + check digits) used for cross-checks.""" + + +class IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation(BaseModel): + """Geocoded coordinates of the document address.""" + + latitude: float | None = None + + longitude: float | None = None + + +class IdVerificationVerifyResponseIdVerificationParsedAddress(BaseModel): + """Structured, geocoded breakdown of `address`. `null` when the document carries no address or it could not be parsed.""" + + street_1: str | None = None + + street_2: str | None = None + + city: str | None = None + + region: str | None = None + + country: str | None = None + """ISO 3166-1 alpha-2 country code.""" + + postal_code: str | None = None + + address_type: str | None = None + + formatted_address: str | None = None + """Canonical geocoded address string.""" + + raw_results: dict[str, object] | None = None + """Raw geocoding payload (`address_components`, `geometry.location`, `place_id`, …) for advanced consumers.""" + + document_location: ( + IdVerificationVerifyResponseIdVerificationParsedAddressDocumentLocation | None + ) = None + """Geocoded coordinates of the document address.""" + + label: str | None = None + """Which document field the address was read from.""" + + is_verified: bool | None = None + """Whether geocoding confirmed the address exists.""" + + category: str | None = None + """Address category (e.g. `Residential`, `Commercial`).""" + + +class IdVerificationVerifyResponseIdVerificationWarnings(BaseModel): + risk: str | None = None + """Machine-readable risk code. Always-decline risks: `DOCUMENT_EXPIRED`, `MINIMUM_AGE_NOT_MET`, `PORTRAIT_IMAGE_NOT_DETECTED`, `SCREEN_CAPTURE_DETECTED`, `PRINTED_COPY_DETECTED`, `PORTRAIT_MANIPULATION_DETECTED`, `PUBLIC_DOCUMENT_IMAGE_DETECTED`, `INVALID_DATE`, `COULD_NOT_RECOGNIZE_DOCUMENT` (400-only, never a warning), `DOCUMENT_NUMBER_NOT_DETECTED`, `COULD_NOT_DETECT_DOCUMENT_TYPE`, `NAME_NOT_DETECTED`, `DATE_OF_BIRTH_NOT_DETECTED`. Governed by request options: `MRZ_NOT_DETECTED` / `MRZ_VALIDATION_FAILED` (`invalid_mrz_action`), `DATA_INCONSISTENT` / `MRZ_AND_DATA_EXTRACTED_FROM_OCR_NOT_SAME` / `DOCUMENT_NAME_DIFFERENT_FROM_OTHER_APPROVED_DOCUMENTS` (`inconsistent_data_action`), `EXPIRATION_DATE_NOT_DETECTED` (`expiration_date_not_detected_action`). Other risks (e.g. `POSSIBLE_DUPLICATED_USER`) are informational.""" + + feature: Literal["ID_VERIFICATION"] | str | None = None + """Feature that raised the warning. Always `ID_VERIFICATION` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """Extra context for some risks (e.g. `POSSIBLE_DUPLICATED_USER` includes `duplicated_session_id`); `null` for most.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint - its options only accept `DECLINE` or `NO_ACTION`.)""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class IdVerificationVerifyResponseIdVerification(BaseModel): + status: Literal["Approved", "Declined"] | str | None = None + """`Approved` when no warning resolves to a decline; `Declined` otherwise - the `warnings` list explains why.""" + + document_type: str | None = None + """Detected document type display name: `Identity Card`, `Passport`, `Driver's License`, `Residence Permit`, `Health Insurance Card`, or `Tax Card`. `null` when the document could not be classified.""" + + document_subtype: str | None = None + """Finer-grained subtype when available (e.g. `ID_CARD_GENERIC`, `EPASSPORT`).""" + + document_number: str | None = None + + personal_number: str | None = None + """Secondary personal/national number printed on some documents.""" + + portrait_image: str | None = None + """Cropped portrait photo from the document. A short-lived media URL (`https:///ocr/...`) when `save_api_request=true` (default); an inline base64-encoded JPEG string when `save_api_request=false`. `null` when no portrait was detected.""" + + front_image: str | None = None + """Aligned crop of the document front. Media URL when saved, inline base64 JPEG otherwise.""" + + back_image: str | None = None + """Aligned crop of the document back. `null` when no `back_image` was uploaded.""" + + front_image_camera_front: str | None = None + """Always `null` on this endpoint (populated only in camera-capture verification sessions).""" + + back_image_camera_front: str | None = None + """Always `null` on this endpoint.""" + + front_image_camera_front_face_match_score: float | None = None + """Always `null` on this endpoint.""" + + back_image_camera_front_face_match_score: float | None = None + """Always `null` on this endpoint.""" + + front_image_quality_score: ( + IdVerificationVerifyResponseIdVerificationFrontImageQualityScore | None + ) = None + """Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.""" + + back_image_quality_score: ( + IdVerificationVerifyResponseIdVerificationBackImageQualityScore | None + ) = None + """Capture-quality diagnostics for the uploaded image (`null` when the side was not provided or scoring failed). All scores are 0–100.""" + + date_of_birth: _dt.date | None = None + + age: int | None = None + """Holder age in years, computed from `date_of_birth`.""" + + expiration_date: _dt.date | None = None + + date_of_issue: _dt.date | None = None + + issuing_state: str | None = None + """Issuing country (ISO 3166-1 alpha-3).""" + + issuing_state_name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + full_name: str | None = None + + gender: str | None = None + """`M`, `F`, or `null` when not printed/detected.""" + + address: str | None = None + """Address exactly as printed on the document.""" + + formatted_address: str | None = None + """Geocoded, normalized version of `address`.""" + + place_of_birth: str | None = None + + marital_status: str | None = None + """`UNKNOWN` unless the document prints marital status.""" + + nationality: str | None = None + """Holder nationality (ISO 3166-1 alpha-3).""" + + extra_fields: dict[str, object] | None = None + """Document-specific extras that have no dedicated column (e.g. `first_surname`/`second_surname` on Spanish IDs, license classes on driving licenses).""" + + mrz: IdVerificationVerifyResponseIdVerificationMrz | None = None + """Parsed machine-readable zone. An **empty object** (`{}`) when the document has no MRZ or it could not be read - also check the `warnings` list in that case. Dates inside the MRZ use the raw `YYMMDD` format.""" + + parsed_address: IdVerificationVerifyResponseIdVerificationParsedAddress | None = None + """Structured, geocoded breakdown of `address`. `null` when the document carries no address or it could not be parsed.""" + + warnings: list[IdVerificationVerifyResponseIdVerificationWarnings] | None = None + """Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`.""" + + barcodes: list[IdVerificationVerifyResponseIdVerificationBarcodes] | None = None + """Barcodes decoded from the document (PDF417, QR, …). Empty when the document has none or none could be read.""" + + +class IdVerificationVerifyResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + id_verification: IdVerificationVerifyResponseIdVerification | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """ISO 8601 timestamp (UTC) of when the response was generated.""" diff --git a/src/didit/types/id_wallets/__init__.py b/src/didit/types/id_wallets/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/id_wallets/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/id_wallets/start_audkenni_params.py b/src/didit/types/id_wallets/start_audkenni_params.py new file mode 100644 index 0000000..fe33197 --- /dev/null +++ b/src/didit/types/id_wallets/start_audkenni_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_audkenni_wallet_verification_start_country_enum import ( + DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartAudkenniParams"] + + +class IdWalletsStartAudkenniParams(TypedDict, total=False): + """Start a Auðkenni verification (ISL).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiAudkenniWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the Auðkenni identity. Defaults to ISL, the only country it serves. + + * `ISL` - ISL + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_bankid_cz_params.py b/src/didit/types/id_wallets/start_bankid_cz_params.py new file mode 100644 index 0000000..95d5781 --- /dev/null +++ b/src/didit/types/id_wallets/start_bankid_cz_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_e2d_enum import ( + DigitalIdWalletApiCountryE2dEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartBankidCzParams"] + + +class IdWalletsStartBankidCzParams(TypedDict, total=False): + """Start a Bank iD verification (CZE).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryE2dEnum + """ISO 3166-1 alpha-3 country of the Bank iD identity. Defaults to CZE, the only country it serves. + + * `CZE` - CZE + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_bankid_no_params.py b/src/didit/types/id_wallets/start_bankid_no_params.py new file mode 100644 index 0000000..9cbddec --- /dev/null +++ b/src/didit/types/id_wallets/start_bankid_no_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_afc_enum import ( + DigitalIdWalletApiCountryAfcEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartBankidNoParams"] + + +class IdWalletsStartBankidNoParams(TypedDict, total=False): + """Start a BankID verification (NOR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryAfcEnum + """ISO 3166-1 alpha-3 country of the BankID identity. Defaults to NOR, the only country it serves. + + * `NOR` - NOR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_bankid_se_params.py b/src/didit/types/id_wallets/start_bankid_se_params.py new file mode 100644 index 0000000..c4170c0 --- /dev/null +++ b/src/didit/types/id_wallets/start_bankid_se_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_ee9_enum import ( + DigitalIdWalletApiCountryEe9Enum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartBankidSeParams"] + + +class IdWalletsStartBankidSeParams(TypedDict, total=False): + """Start a BankID verification (SWE).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryEe9Enum + """ISO 3166-1 alpha-3 country of the BankID identity. Defaults to SWE, the only country it serves. + + * `SWE` - SWE + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_buypass_params.py b/src/didit/types/id_wallets/start_buypass_params.py new file mode 100644 index 0000000..c81d5f2 --- /dev/null +++ b/src/didit/types/id_wallets/start_buypass_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_afc_enum import ( + DigitalIdWalletApiCountryAfcEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartBuypassParams"] + + +class IdWalletsStartBuypassParams(TypedDict, total=False): + """Start a Buypass ID verification (NOR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryAfcEnum + """ISO 3166-1 alpha-3 country of the Buypass ID identity. Defaults to NOR, the only country it serves. + + * `NOR` - NOR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_connectid_params.py b/src/didit/types/id_wallets/start_connectid_params.py new file mode 100644 index 0000000..d214f09 --- /dev/null +++ b/src/didit/types/id_wallets/start_connectid_params.py @@ -0,0 +1,45 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_connectid_wallet_verification_start_country_enum import ( + DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartConnectidParams"] + + +class IdWalletsStartConnectidParams(TypedDict, total=False): + """Start a ConnectID verification (AUS).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiConnectidWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the ConnectID identity. Defaults to AUS, the only country it serves. + + * `AUS` - AUS + """ + + authorisation_server_id: str + """Authorisation server ID selected with ConnectID's bank selector. Sandbox defaults to Mock Bank A.""" + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_diia_params.py b/src/didit/types/id_wallets/start_diia_params.py new file mode 100644 index 0000000..48f132a --- /dev/null +++ b/src/didit/types/id_wallets/start_diia_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_diia_wallet_verification_start_country_enum import ( + DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartDiiaParams"] + + +class IdWalletsStartDiiaParams(TypedDict, total=False): + """Start a Diia verification (UKR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiDiiaWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the Diia identity. Defaults to UKR, the only country it serves. + + * `UKR` - UKR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_eparaksts_mobile_params.py b/src/didit/types/id_wallets/start_eparaksts_mobile_params.py new file mode 100644 index 0000000..dffb278 --- /dev/null +++ b/src/didit/types/id_wallets/start_eparaksts_mobile_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_eparaksts_mobile_wallet_verification_start_country_enum import ( + DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartEparakstsMobileParams"] + + +class IdWalletsStartEparakstsMobileParams(TypedDict, total=False): + """Start a eParaksts mobile verification (LVA).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiEparakstsMobileWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the eParaksts mobile identity. Defaults to LVA, the only country it serves. + + * `LVA` - LVA + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_eudi_params.py b/src/didit/types/id_wallets/start_eudi_params.py new file mode 100644 index 0000000..ae9069d --- /dev/null +++ b/src/didit/types/id_wallets/start_eudi_params.py @@ -0,0 +1,71 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_eudi_wallet_verification_start_country_enum import ( + DigitalIdWalletApiEudiWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartEudiParams"] + + +class IdWalletsStartEudiParams(TypedDict, total=False): + """Start a EUDI Wallet verification (AUT, BEL, BGR, HRV, CYP, CZE, DNK, EST, FIN, FRA, DEU, GRC, HUN, IRL, ITA, LVA, LTU, LUX, MLT, NLD, POL, PRT, ROU, SVK, SVN, ESP, SWE, ISL, LIE, NOR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: Required[DigitalIdWalletApiEudiWalletVerificationStartCountryEnum] + """ISO 3166-1 alpha-3 country of the EUDI Wallet identity. + + * `AUT` - AUT + * `BEL` - BEL + * `BGR` - BGR + * `HRV` - HRV + * `CYP` - CYP + * `CZE` - CZE + * `DNK` - DNK + * `EST` - EST + * `FIN` - FIN + * `FRA` - FRA + * `DEU` - DEU + * `GRC` - GRC + * `HUN` - HUN + * `IRL` - IRL + * `ITA` - ITA + * `LVA` - LVA + * `LTU` - LTU + * `LUX` - LUX + * `MLT` - MLT + * `NLD` - NLD + * `POL` - POL + * `PRT` - PRT + * `ROU` - ROU + * `SVK` - SVK + * `SVN` - SVN + * `ESP` - ESP + * `SWE` - SWE + * `ISL` - ISL + * `LIE` - LIE + * `NOR` - NOR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_franceconnect_params.py b/src/didit/types/id_wallets/start_franceconnect_params.py new file mode 100644 index 0000000..dc76df2 --- /dev/null +++ b/src/didit/types/id_wallets/start_franceconnect_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_franceconnect_wallet_verification_start_country_enum import ( + DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartFranceconnectParams"] + + +class IdWalletsStartFranceconnectParams(TypedDict, total=False): + """Start a FranceConnect verification (FRA).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiFranceconnectWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the FranceConnect identity. Defaults to FRA, the only country it serves. + + * `FRA` - FRA + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_frejaid_params.py b/src/didit/types/id_wallets/start_frejaid_params.py new file mode 100644 index 0000000..402d275 --- /dev/null +++ b/src/didit/types/id_wallets/start_frejaid_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_ee9_enum import ( + DigitalIdWalletApiCountryEe9Enum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartFrejaidParams"] + + +class IdWalletsStartFrejaidParams(TypedDict, total=False): + """Start a Freja eID verification (SWE).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryEe9Enum + """ISO 3166-1 alpha-3 country of the Freja eID identity. Defaults to SWE, the only country it serves. + + * `SWE` - SWE + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_ftn_params.py b/src/didit/types/id_wallets/start_ftn_params.py new file mode 100644 index 0000000..4f03730 --- /dev/null +++ b/src/didit/types/id_wallets/start_ftn_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_ftn_wallet_verification_start_country_enum import ( + DigitalIdWalletApiFtnWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartFtnParams"] + + +class IdWalletsStartFtnParams(TypedDict, total=False): + """Start a Finnish Trust Network verification (FIN).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiFtnWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the Finnish Trust Network identity. Defaults to FIN, the only country it serves. + + * `FIN` - FIN + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_govbr_params.py b/src/didit/types/id_wallets/start_govbr_params.py new file mode 100644 index 0000000..b589a0d --- /dev/null +++ b/src/didit/types/id_wallets/start_govbr_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_govbr_wallet_verification_start_country_enum import ( + DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartGovbrParams"] + + +class IdWalletsStartGovbrParams(TypedDict, total=False): + """Start a gov.br verification (BRA).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiGovbrWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the gov.br identity. Defaults to BRA, the only country it serves. + + * `BRA` - BRA + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_govuk_wallet_params.py b/src/didit/types/id_wallets/start_govuk_wallet_params.py new file mode 100644 index 0000000..76b6195 --- /dev/null +++ b/src/didit/types/id_wallets/start_govuk_wallet_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country520_enum import ( + DigitalIdWalletApiCountry520Enum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartGovukWalletParams"] + + +class IdWalletsStartGovukWalletParams(TypedDict, total=False): + """Start a GOV.UK Wallet verification (GBR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountry520Enum + """ISO 3166-1 alpha-3 country of the GOV.UK Wallet identity. Defaults to GBR, the only country it serves. + + * `GBR` - GBR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_idcard_ee_params.py b/src/didit/types/id_wallets/start_idcard_ee_params.py new file mode 100644 index 0000000..3a44285 --- /dev/null +++ b/src/didit/types/id_wallets/start_idcard_ee_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_idcard_ee_wallet_verification_start_country_enum import ( + DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartIdcardEeParams"] + + +class IdWalletsStartIdcardEeParams(TypedDict, total=False): + """Start a Estonian ID-card verification (EST).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiIdcardEeWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the Estonian ID-card identity. Defaults to EST, the only country it serves. + + * `EST` - EST + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_idin_params.py b/src/didit/types/id_wallets/start_idin_params.py new file mode 100644 index 0000000..4cb8bbc --- /dev/null +++ b/src/didit/types/id_wallets/start_idin_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_idin_wallet_verification_start_country_enum import ( + DigitalIdWalletApiIdinWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartIdinParams"] + + +class IdWalletsStartIdinParams(TypedDict, total=False): + """Start a iDIN verification (NLD).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiIdinWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the iDIN identity. Defaults to NLD, the only country it serves. + + * `NLD` - NLD + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_itsme_params.py b/src/didit/types/id_wallets/start_itsme_params.py new file mode 100644 index 0000000..cba291f --- /dev/null +++ b/src/didit/types/id_wallets/start_itsme_params.py @@ -0,0 +1,44 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_itsme_wallet_verification_start_country_enum import ( + DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartItsmeParams"] + + +class IdWalletsStartItsmeParams(TypedDict, total=False): + """Start a itsme verification (BEL, LUX, NLD).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: Required[DigitalIdWalletApiItsmeWalletVerificationStartCountryEnum] + """ISO 3166-1 alpha-3 country of the itsme identity. + + * `BEL` - BEL + * `LUX` - LUX + * `NLD` - NLD + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_mitid_params.py b/src/didit/types/id_wallets/start_mitid_params.py new file mode 100644 index 0000000..5568857 --- /dev/null +++ b/src/didit/types/id_wallets/start_mitid_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_mitid_wallet_verification_start_country_enum import ( + DigitalIdWalletApiMitidWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartMitidParams"] + + +class IdWalletsStartMitidParams(TypedDict, total=False): + """Start a MitID verification (DNK).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiMitidWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the MitID identity. Defaults to DNK, the only country it serves. + + * `DNK` - DNK + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_mobile_id_params.py b/src/didit/types/id_wallets/start_mobile_id_params.py new file mode 100644 index 0000000..7c1f92f --- /dev/null +++ b/src/didit/types/id_wallets/start_mobile_id_params.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_mobile_id_wallet_verification_start_country_enum import ( + DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartMobileIdParams"] + + +class IdWalletsStartMobileIdParams(TypedDict, total=False): + """Start a Mobile-ID verification (EST, LTU).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: Required[DigitalIdWalletApiMobileIdWalletVerificationStartCountryEnum] + """ISO 3166-1 alpha-3 country of the Mobile-ID identity. + + * `EST` - EST + * `LTU` - LTU + """ + + personal_code: Required[str] + """The user's national personal identification code. It addresses the user's Mobile-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN.""" + + phone_number: Required[str] + """The Mobile-ID phone number in E.164 format (+372... or +370...).""" diff --git a/src/didit/types/id_wallets/start_mojeid_params.py b/src/didit/types/id_wallets/start_mojeid_params.py new file mode 100644 index 0000000..78cc45a --- /dev/null +++ b/src/didit/types/id_wallets/start_mojeid_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_e2d_enum import ( + DigitalIdWalletApiCountryE2dEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartMojeidParams"] + + +class IdWalletsStartMojeidParams(TypedDict, total=False): + """Start a MojeID verification (CZE).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryE2dEnum + """ISO 3166-1 alpha-3 country of the MojeID identity. Defaults to CZE, the only country it serves. + + * `CZE` - CZE + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_oneid_params.py b/src/didit/types/id_wallets/start_oneid_params.py new file mode 100644 index 0000000..f912372 --- /dev/null +++ b/src/didit/types/id_wallets/start_oneid_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country520_enum import ( + DigitalIdWalletApiCountry520Enum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartOneidParams"] + + +class IdWalletsStartOneidParams(TypedDict, total=False): + """Start a OneID verification (GBR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountry520Enum + """ISO 3166-1 alpha-3 country of the OneID identity. Defaults to GBR, the only country it serves. + + * `GBR` - GBR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_personalausweis_params.py b/src/didit/types/id_wallets/start_personalausweis_params.py new file mode 100644 index 0000000..9a4b157 --- /dev/null +++ b/src/didit/types/id_wallets/start_personalausweis_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_personalausweis_wallet_verification_start_country_enum import ( + DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartPersonalausweisParams"] + + +class IdWalletsStartPersonalausweisParams(TypedDict, total=False): + """Start a Personalausweis verification (DEU).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiPersonalausweisWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the Personalausweis identity. Defaults to DEU, the only country it serves. + + * `DEU` - DEU + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_smart_id_params.py b/src/didit/types/id_wallets/start_smart_id_params.py new file mode 100644 index 0000000..aa43200 --- /dev/null +++ b/src/didit/types/id_wallets/start_smart_id_params.py @@ -0,0 +1,37 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_smart_id_wallet_verification_start_country_enum import ( + DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartSmartIdParams"] + + +class IdWalletsStartSmartIdParams(TypedDict, total=False): + """Start a Smart-ID verification (EST, LVA, LTU, BEL).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: Required[DigitalIdWalletApiSmartIdWalletVerificationStartCountryEnum] + """ISO 3166-1 alpha-3 country of the Smart-ID identity. + + * `EST` - EST + * `LVA` - LVA + * `LTU` - LTU + * `BEL` - BEL + """ + + personal_code: Required[str] + """The user's national personal identification code. It addresses the user's Smart-ID app; the user confirms the comparison code and enters their PIN in the app. Never collect the PIN.""" diff --git a/src/didit/types/id_wallets/start_uae_pass_params.py b/src/didit/types/id_wallets/start_uae_pass_params.py new file mode 100644 index 0000000..f39c195 --- /dev/null +++ b/src/didit/types/id_wallets/start_uae_pass_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum +from didit.types.components.digital_id_wallet_api_uae_pass_wallet_verification_start_country_enum import ( + DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum, +) + +__all__ = ["IdWalletsStartUaePassParams"] + + +class IdWalletsStartUaePassParams(TypedDict, total=False): + """Start a UAE PASS verification (ARE).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiUaePassWalletVerificationStartCountryEnum + """ISO 3166-1 alpha-3 country of the UAE PASS identity. Defaults to ARE, the only country it serves. + + * `ARE` - ARE + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/id_wallets/start_vipps_params.py b/src/didit/types/id_wallets/start_vipps_params.py new file mode 100644 index 0000000..129d994 --- /dev/null +++ b/src/didit/types/id_wallets/start_vipps_params.py @@ -0,0 +1,42 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.digital_id_wallet_api_country_afc_enum import ( + DigitalIdWalletApiCountryAfcEnum, +) +from didit.types.components.digital_id_wallet_api_device_enum import DigitalIdWalletApiDeviceEnum + +__all__ = ["IdWalletsStartVippsParams"] + + +class IdWalletsStartVippsParams(TypedDict, total=False): + """Start a Vipps verification (NOR).""" + + vendor_data: str | None + """Your own reference for this verification (e.g. your user id). Returned in results and webhooks.""" + + metadata: Mapping[str, object] | None + """Optional JSON object stored with the verification and returned in results and webhooks.""" + + sandbox_scenario: str + """Sandbox applications only: the simulated outcome - 'wallet_cancelled', 'wallet_timeout' or 'wallet_provider_error'; any other scenario (or none) verifies. Rejected on live applications. The full catalog is available at GET /v1/sandbox/scenarios/.""" + + country: DigitalIdWalletApiCountryAfcEnum + """ISO 3166-1 alpha-3 country of the Vipps identity. Defaults to NOR, the only country it serves. + + * `NOR` - NOR + """ + + return_url: Required[str] + """https URL the user's browser returns to after the wallet, with request_id and result appended. Its host must be registered in the application's allowed_return_hosts (or be its white-label domain). Never trust result on its own: read the outcome with GET .../verifications/{request_id}/ or the webhook.""" + + device: DigitalIdWalletApiDeviceEnum + """same_device when the user signs in on the device showing your page, cross_device when they use another one (e.g. BankID's QR code). + + * `same_device` - same_device + * `cross_device` - cross_device + """ diff --git a/src/didit/types/ip/__init__.py b/src/didit/types/ip/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/ip/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/ip/assess_risk_params.py b/src/didit/types/ip/assess_risk_params.py new file mode 100644 index 0000000..fd57066 --- /dev/null +++ b/src/didit/types/ip/assess_risk_params.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["IpAssessRiskParams"] + + +class IpAssessRiskParams(TypedDict, total=False): + ip_address: Required[str] + """IPv4 or IPv6 address to score.""" + + claimed_country: str + """Optional ISO 3166-1 alpha-2 or alpha-3 country claimed by the user or session. Used to calculate `country_mismatch`.""" + + user_agent: str + """User agent observed by your backend. Stored as request context; it does not provide device fingerprinting.""" + + vendor_data: str + """Your stable reference for this request. Stored on the API session and echoed in the response.""" + + metadata: Mapping[str, object] | None + """Your non-sensitive JSON metadata. Stored on the API session and echoed in the response.""" diff --git a/src/didit/types/kyb/__init__.py b/src/didit/types/kyb/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/kyb/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/kyb/list_shareholders_params.py b/src/didit/types/kyb/list_shareholders_params.py new file mode 100644 index 0000000..3b419bf --- /dev/null +++ b/src/didit/types/kyb/list_shareholders_params.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["KybListShareholdersParams"] + + +class KybListShareholdersParams(TypedDict, total=False): + kyb_response_id: Required[str] + """Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it.""" + + vendor_data: str + """Your identifier for this company; echoed back and stored with the business session.""" + + metadata: Mapping[str, object] | None + """Free-form JSON stored with the business session and echoed back.""" + + save_api_request: bool + """Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`.""" diff --git a/src/didit/types/kyb/list_shareholders_response.py b/src/didit/types/kyb/list_shareholders_response.py new file mode 100644 index 0000000..1e666f1 --- /dev/null +++ b/src/didit/types/kyb/list_shareholders_response.py @@ -0,0 +1,684 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.kyb_registry_address import KYBRegistryAddress +from didit.types.components.kyb_registry_industry import KYBRegistryIndustry +from didit.types.components.kyb_registry_ownership import KYBRegistryOwnership +from didit.types.components.kyb_registry_shareholding import KYBRegistryShareholding + +__all__ = [ + "KybListShareholdersResponse", + "KybListShareholdersResponse202", + "KybListShareholdersResponse202KybRegistry", + "KybListShareholdersResponse202KybRegistryBeneficialOwners", + "KybListShareholdersResponse202KybRegistryOfficers", + "KybListShareholdersResponseKybRegistry", + "KybListShareholdersResponseKybRegistryBeneficialOwners", + "KybListShareholdersResponseKybRegistryOfficers", +] + + +class KybListShareholdersResponseKybRegistryBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + """Whether the owner is a natural person or a corporate entity.""" + + roles: list[str] | None = None + """Normalized roles (e.g. `ubo`, `shareholder`).""" + + ownership_min_shares: str | None = None + """Lower bound of the ownership band reported by the registry (e.g. `25%`).""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership band reported by the registry.""" + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + effective_ownership_percent: float | None = None + """Parsed ownership percentage (max bound, falling back to min bound).""" + + company_name: str | None = None + + registration_number: str | None = None + + country_of_birth: str | None = None + + email: str | None = None + + nationality: str | None = None + + designation: str | None = None + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + shareholding: KYBRegistryShareholding | None = None + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + +class KybListShareholdersResponseKybRegistryOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + + designation: str | None = None + """Raw registry designation (e.g. `Director`, `Secretary`).""" + + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other", + ] + | str + | None + ) = None + """Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.""" + + nationality: str | None = None + + is_active: bool | None = None + """`false` when the registry marks the appointment as resigned.""" + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + """Status of an associated KYC session, when one was started for this officer. `null` otherwise.""" + + kyc_session_url: str | None = None + """Verification URL of the associated KYC session, when one exists.""" + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + address: str | None = None + """Officer address reported by the registry.""" + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class KybListShareholdersResponseKybRegistry(BaseModel): + """Full registry company profile.""" + + uuid: str | None = None + """Didit's id for this company record.""" + + node_id: str | None = None + + status: Literal["Not Finished", "Approved", "Declined", "In Review"] | str | None = None + """Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived).""" + + registry_status: ( + Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + ) = None + """Normalized company status as reported by the registry.""" + + data_resolved: bool | None = None + """`true` once the registry returned the full profile. `false` when the registry was still processing after polling - the stored record completes automatically once the registry resolves.""" + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """ISO 3166-1 alpha-2.""" + + region: str | None = None + """Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise.""" + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + + tax_number: str | None = None + + risk_level: str | None = None + """Registry-reported risk level, when available.""" + + verification_status: str | None = None + """Registry verification status, when available.""" + + is_from_registry: bool | None = None + """Always `true` for this endpoint - the profile was retrieved from an official registry.""" + + fetch_status: Literal["pending", "resolved"] | str | None = None + """Registry fetch lifecycle. Mirrored by `data_resolved`.""" + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + """Activity description or classification code, when reported.""" + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Decimal amount as a string, when reported.""" + + registered_capital_currency: str | None = None + """ISO 4217 code, when reported.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + """LEI, when reported.""" + + location_of_registration: str | None = None + """Registry/jurisdiction the company is registered with, when reported.""" + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set - VAT and tax ID validation runs when the end user submits the registry step in the hosted flow.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + """Registry-reported share/capital details (currency, total shares, share classes), when available.""" + + officers: list[KybListShareholdersResponseKybRegistryOfficers] | None = None + """Directors, secretaries, and other officers extracted from the registry.""" + + beneficial_owners: list[KybListShareholdersResponseKybRegistryBeneficialOwners] | None = None + """Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership.""" + + addresses: list[KYBRegistryAddress] | None = None + """Every address the registry states, one canonical shape. Null when it states none.""" + + industries: list[KYBRegistryIndustry] | None = None + """The activities the registry classifies the company under, one canonical shape. Null when it states none.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead.""" + + user_provided_data: dict[str, object] | None = None + """Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data).""" + + confirmed_by_user_at: _dt.datetime | None = None + """Always `null` on this endpoint.""" + + last_console_edit_at: _dt.datetime | None = None + """Set when a console reviewer edits the record later.""" + + is_editable: bool | None = None + """Whether the registry-sourced data can still be edited in the hosted flow/console.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """Same as the top-level `registry_tier_delivered`.""" + + registry_ownership_status: ( + Literal["not_requested", "delivered", "unavailable", "pending"] | str | None + ) = None + """`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier.""" + + ownership: KYBRegistryOwnership | None = None + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + registry_tier: str | None = None + """The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`.""" + + +class KybListShareholdersResponse(BaseModel): + request_id: str | None = None + """Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response.""" + + registry_tier_requested: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company.""" + + kyb_registry: KybListShareholdersResponseKybRegistry | None = None + """Full registry company profile.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None + + +class KybListShareholdersResponse202KybRegistryBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + """Whether the owner is a natural person or a corporate entity.""" + + roles: list[str] | None = None + """Normalized roles (e.g. `ubo`, `shareholder`).""" + + ownership_min_shares: str | None = None + """Lower bound of the ownership band reported by the registry (e.g. `25%`).""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership band reported by the registry.""" + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + effective_ownership_percent: float | None = None + """Parsed ownership percentage (max bound, falling back to min bound).""" + + company_name: str | None = None + + registration_number: str | None = None + + country_of_birth: str | None = None + + email: str | None = None + + nationality: str | None = None + + designation: str | None = None + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + shareholding: KYBRegistryShareholding | None = None + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + +class KybListShareholdersResponse202KybRegistryOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + + designation: str | None = None + """Raw registry designation (e.g. `Director`, `Secretary`).""" + + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other", + ] + | str + | None + ) = None + """Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.""" + + nationality: str | None = None + + is_active: bool | None = None + """`false` when the registry marks the appointment as resigned.""" + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + """Status of an associated KYC session, when one was started for this officer. `null` otherwise.""" + + kyc_session_url: str | None = None + """Verification URL of the associated KYC session, when one exists.""" + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + address: str | None = None + """Officer address reported by the registry.""" + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class KybListShareholdersResponse202KybRegistry(BaseModel): + """Full registry company profile.""" + + uuid: str | None = None + """Didit's id for this company record.""" + + node_id: str | None = None + + status: Literal["Not Finished", "Approved", "Declined", "In Review"] | str | None = None + """Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived).""" + + registry_status: ( + Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + ) = None + """Normalized company status as reported by the registry.""" + + data_resolved: bool | None = None + """`true` once the registry returned the full profile. `false` when the registry was still processing after polling - the stored record completes automatically once the registry resolves.""" + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """ISO 3166-1 alpha-2.""" + + region: str | None = None + """Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise.""" + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + + tax_number: str | None = None + + risk_level: str | None = None + """Registry-reported risk level, when available.""" + + verification_status: str | None = None + """Registry verification status, when available.""" + + is_from_registry: bool | None = None + """Always `true` for this endpoint - the profile was retrieved from an official registry.""" + + fetch_status: Literal["pending", "resolved"] | str | None = None + """Registry fetch lifecycle. Mirrored by `data_resolved`.""" + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + """Activity description or classification code, when reported.""" + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Decimal amount as a string, when reported.""" + + registered_capital_currency: str | None = None + """ISO 4217 code, when reported.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + """LEI, when reported.""" + + location_of_registration: str | None = None + """Registry/jurisdiction the company is registered with, when reported.""" + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set - VAT and tax ID validation runs when the end user submits the registry step in the hosted flow.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + """Registry-reported share/capital details (currency, total shares, share classes), when available.""" + + officers: list[KybListShareholdersResponse202KybRegistryOfficers] | None = None + """Directors, secretaries, and other officers extracted from the registry.""" + + beneficial_owners: list[KybListShareholdersResponse202KybRegistryBeneficialOwners] | None = None + """Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership.""" + + addresses: list[KYBRegistryAddress] | None = None + """Every address the registry states, one canonical shape. Null when it states none.""" + + industries: list[KYBRegistryIndustry] | None = None + """The activities the registry classifies the company under, one canonical shape. Null when it states none.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead.""" + + user_provided_data: dict[str, object] | None = None + """Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data).""" + + confirmed_by_user_at: _dt.datetime | None = None + """Always `null` on this endpoint.""" + + last_console_edit_at: _dt.datetime | None = None + """Set when a console reviewer edits the record later.""" + + is_editable: bool | None = None + """Whether the registry-sourced data can still be edited in the hosted flow/console.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """Same as the top-level `registry_tier_delivered`.""" + + registry_ownership_status: ( + Literal["not_requested", "delivered", "unavailable", "pending"] | str | None + ) = None + """`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier.""" + + ownership: KYBRegistryOwnership | None = None + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + registry_tier: str | None = None + """The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`.""" + + +class KybListShareholdersResponse202(BaseModel): + request_id: str | None = None + """Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response.""" + + registry_tier_requested: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company.""" + + kyb_registry: KybListShareholdersResponse202KybRegistry | None = None + """Full registry company profile.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/kyb/list_ubos_params.py b/src/didit/types/kyb/list_ubos_params.py new file mode 100644 index 0000000..0d2f885 --- /dev/null +++ b/src/didit/types/kyb/list_ubos_params.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["KybListUbosParams"] + + +class KybListUbosParams(TypedDict, total=False): + kyb_response_id: Required[str] + """Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it.""" + + vendor_data: str + """Your identifier for this company; echoed back and stored with the business session.""" + + metadata: Mapping[str, object] | None + """Free-form JSON stored with the business session and echoed back.""" + + save_api_request: bool + """Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`.""" diff --git a/src/didit/types/kyb/list_ubos_response.py b/src/didit/types/kyb/list_ubos_response.py new file mode 100644 index 0000000..aa8c5d6 --- /dev/null +++ b/src/didit/types/kyb/list_ubos_response.py @@ -0,0 +1,684 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.kyb_registry_address import KYBRegistryAddress +from didit.types.components.kyb_registry_industry import KYBRegistryIndustry +from didit.types.components.kyb_registry_ownership import KYBRegistryOwnership +from didit.types.components.kyb_registry_shareholding import KYBRegistryShareholding + +__all__ = [ + "KybListUbosResponse", + "KybListUbosResponse202", + "KybListUbosResponse202KybRegistry", + "KybListUbosResponse202KybRegistryBeneficialOwners", + "KybListUbosResponse202KybRegistryOfficers", + "KybListUbosResponseKybRegistry", + "KybListUbosResponseKybRegistryBeneficialOwners", + "KybListUbosResponseKybRegistryOfficers", +] + + +class KybListUbosResponseKybRegistryBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + """Whether the owner is a natural person or a corporate entity.""" + + roles: list[str] | None = None + """Normalized roles (e.g. `ubo`, `shareholder`).""" + + ownership_min_shares: str | None = None + """Lower bound of the ownership band reported by the registry (e.g. `25%`).""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership band reported by the registry.""" + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + effective_ownership_percent: float | None = None + """Parsed ownership percentage (max bound, falling back to min bound).""" + + company_name: str | None = None + + registration_number: str | None = None + + country_of_birth: str | None = None + + email: str | None = None + + nationality: str | None = None + + designation: str | None = None + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + shareholding: KYBRegistryShareholding | None = None + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + +class KybListUbosResponseKybRegistryOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + + designation: str | None = None + """Raw registry designation (e.g. `Director`, `Secretary`).""" + + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other", + ] + | str + | None + ) = None + """Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.""" + + nationality: str | None = None + + is_active: bool | None = None + """`false` when the registry marks the appointment as resigned.""" + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + """Status of an associated KYC session, when one was started for this officer. `null` otherwise.""" + + kyc_session_url: str | None = None + """Verification URL of the associated KYC session, when one exists.""" + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + address: str | None = None + """Officer address reported by the registry.""" + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class KybListUbosResponseKybRegistry(BaseModel): + """Full registry company profile.""" + + uuid: str | None = None + """Didit's id for this company record.""" + + node_id: str | None = None + + status: Literal["Not Finished", "Approved", "Declined", "In Review"] | str | None = None + """Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived).""" + + registry_status: ( + Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + ) = None + """Normalized company status as reported by the registry.""" + + data_resolved: bool | None = None + """`true` once the registry returned the full profile. `false` when the registry was still processing after polling - the stored record completes automatically once the registry resolves.""" + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """ISO 3166-1 alpha-2.""" + + region: str | None = None + """Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise.""" + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + + tax_number: str | None = None + + risk_level: str | None = None + """Registry-reported risk level, when available.""" + + verification_status: str | None = None + """Registry verification status, when available.""" + + is_from_registry: bool | None = None + """Always `true` for this endpoint - the profile was retrieved from an official registry.""" + + fetch_status: Literal["pending", "resolved"] | str | None = None + """Registry fetch lifecycle. Mirrored by `data_resolved`.""" + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + """Activity description or classification code, when reported.""" + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Decimal amount as a string, when reported.""" + + registered_capital_currency: str | None = None + """ISO 4217 code, when reported.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + """LEI, when reported.""" + + location_of_registration: str | None = None + """Registry/jurisdiction the company is registered with, when reported.""" + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set - VAT and tax ID validation runs when the end user submits the registry step in the hosted flow.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + """Registry-reported share/capital details (currency, total shares, share classes), when available.""" + + officers: list[KybListUbosResponseKybRegistryOfficers] | None = None + """Directors, secretaries, and other officers extracted from the registry.""" + + beneficial_owners: list[KybListUbosResponseKybRegistryBeneficialOwners] | None = None + """Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership.""" + + addresses: list[KYBRegistryAddress] | None = None + """Every address the registry states, one canonical shape. Null when it states none.""" + + industries: list[KYBRegistryIndustry] | None = None + """The activities the registry classifies the company under, one canonical shape. Null when it states none.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead.""" + + user_provided_data: dict[str, object] | None = None + """Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data).""" + + confirmed_by_user_at: _dt.datetime | None = None + """Always `null` on this endpoint.""" + + last_console_edit_at: _dt.datetime | None = None + """Set when a console reviewer edits the record later.""" + + is_editable: bool | None = None + """Whether the registry-sourced data can still be edited in the hosted flow/console.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """Same as the top-level `registry_tier_delivered`.""" + + registry_ownership_status: ( + Literal["not_requested", "delivered", "unavailable", "pending"] | str | None + ) = None + """`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier.""" + + ownership: KYBRegistryOwnership | None = None + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + registry_tier: str | None = None + """The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`.""" + + +class KybListUbosResponse(BaseModel): + request_id: str | None = None + """Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response.""" + + registry_tier_requested: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, when the registry has no ownership record for this company, or when the resolved report of a `ubo` request names no natural person as a beneficial owner.""" + + kyb_registry: KybListUbosResponseKybRegistry | None = None + """Full registry company profile.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None + + +class KybListUbosResponse202KybRegistryBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + """Whether the owner is a natural person or a corporate entity.""" + + roles: list[str] | None = None + """Normalized roles (e.g. `ubo`, `shareholder`).""" + + ownership_min_shares: str | None = None + """Lower bound of the ownership band reported by the registry (e.g. `25%`).""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership band reported by the registry.""" + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + effective_ownership_percent: float | None = None + """Parsed ownership percentage (max bound, falling back to min bound).""" + + company_name: str | None = None + + registration_number: str | None = None + + country_of_birth: str | None = None + + email: str | None = None + + nationality: str | None = None + + designation: str | None = None + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + shareholding: KYBRegistryShareholding | None = None + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + +class KybListUbosResponse202KybRegistryOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + + designation: str | None = None + """Raw registry designation (e.g. `Director`, `Secretary`).""" + + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other", + ] + | str + | None + ) = None + """Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.""" + + nationality: str | None = None + + is_active: bool | None = None + """`false` when the registry marks the appointment as resigned.""" + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + """Status of an associated KYC session, when one was started for this officer. `null` otherwise.""" + + kyc_session_url: str | None = None + """Verification URL of the associated KYC session, when one exists.""" + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + address: str | None = None + """Officer address reported by the registry.""" + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class KybListUbosResponse202KybRegistry(BaseModel): + """Full registry company profile.""" + + uuid: str | None = None + """Didit's id for this company record.""" + + node_id: str | None = None + + status: Literal["Not Finished", "Approved", "Declined", "In Review"] | str | None = None + """Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived).""" + + registry_status: ( + Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + ) = None + """Normalized company status as reported by the registry.""" + + data_resolved: bool | None = None + """`true` once the registry returned the full profile. `false` when the registry was still processing after polling - the stored record completes automatically once the registry resolves.""" + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """ISO 3166-1 alpha-2.""" + + region: str | None = None + """Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise.""" + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + + tax_number: str | None = None + + risk_level: str | None = None + """Registry-reported risk level, when available.""" + + verification_status: str | None = None + """Registry verification status, when available.""" + + is_from_registry: bool | None = None + """Always `true` for this endpoint - the profile was retrieved from an official registry.""" + + fetch_status: Literal["pending", "resolved"] | str | None = None + """Registry fetch lifecycle. Mirrored by `data_resolved`.""" + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + """Activity description or classification code, when reported.""" + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Decimal amount as a string, when reported.""" + + registered_capital_currency: str | None = None + """ISO 4217 code, when reported.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + """LEI, when reported.""" + + location_of_registration: str | None = None + """Registry/jurisdiction the company is registered with, when reported.""" + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set - VAT and tax ID validation runs when the end user submits the registry step in the hosted flow.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + """Registry-reported share/capital details (currency, total shares, share classes), when available.""" + + officers: list[KybListUbosResponse202KybRegistryOfficers] | None = None + """Directors, secretaries, and other officers extracted from the registry.""" + + beneficial_owners: list[KybListUbosResponse202KybRegistryBeneficialOwners] | None = None + """Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership.""" + + addresses: list[KYBRegistryAddress] | None = None + """Every address the registry states, one canonical shape. Null when it states none.""" + + industries: list[KYBRegistryIndustry] | None = None + """The activities the registry classifies the company under, one canonical shape. Null when it states none.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead.""" + + user_provided_data: dict[str, object] | None = None + """Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data).""" + + confirmed_by_user_at: _dt.datetime | None = None + """Always `null` on this endpoint.""" + + last_console_edit_at: _dt.datetime | None = None + """Set when a console reviewer edits the record later.""" + + is_editable: bool | None = None + """Whether the registry-sourced data can still be edited in the hosted flow/console.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """Same as the top-level `registry_tier_delivered`.""" + + registry_ownership_status: ( + Literal["not_requested", "delivered", "unavailable", "pending"] | str | None + ) = None + """`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `pending` when the ownership section has not been retrieved from the registry yet and Didit is still completing it, `not_requested` on the Lite tier.""" + + ownership: KYBRegistryOwnership | None = None + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + registry_tier: str | None = None + """The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`.""" + + +class KybListUbosResponse202(BaseModel): + request_id: str | None = None + """Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response.""" + + registry_tier_requested: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, when the registry has no ownership record for this company, or when the resolved report of a `ubo` request names no natural person as a beneficial owner.""" + + kyb_registry: KybListUbosResponse202KybRegistry | None = None + """Full registry company profile.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/kyb/search_params.py b/src/didit/types/kyb/search_params.py new file mode 100644 index 0000000..925b9c1 --- /dev/null +++ b/src/didit/types/kyb/search_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["KybSearchParams"] + + +class KybSearchParams(TypedDict, total=False): + country_code: Required[str] + """ISO 3166-1 alpha-2 code of the country whose company registry to search. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` searches the California registry. United States searches always require the subdivision - a bare `US` is rejected. Case-insensitive; normalized to upper case.""" + + name: str + """Company name to search for. Either `name` or `registration_number` is required.""" + + registration_number: str + """Registry registration number to search for. Either `name` or `registration_number` is required.""" + + search_type: Literal["contains", "start_with", "fuzzy"] | str + """Name-matching strategy applied by the registry search.""" + + vendor_data: str + """Your identifier for this lookup; echoed back and included in the webhook callback.""" + + metadata: Mapping[str, object] | None + """Free-form JSON; echoed back and included in the webhook callback.""" + + webhook_url: str + """Switches the search to asynchronous mode: the API returns immediately and POSTs an unsigned `kyb.registry_search.resolved` callback to this URL when the candidate list is ready.""" diff --git a/src/didit/types/kyb/search_response.py b/src/didit/types/kyb/search_response.py new file mode 100644 index 0000000..403769f --- /dev/null +++ b/src/didit/types/kyb/search_response.py @@ -0,0 +1,74 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "KybSearchResponse", + "KybSearchResponseKybRegistry", + "KybSearchResponseKybRegistryCompanies", + "KybSearchResponseKybRegistryPagination", +] + + +class KybSearchResponseKybRegistryCompanies(BaseModel): + kyb_response_id: str | None = None + """Handle for this candidate - pass it to `POST /v3/kyb/select/` to retrieve the full profile. Ephemeral and scoped to this search: do not store it long-term; re-search to get a fresh one.""" + + name: str | None = None + """Registered company name.""" + + registration_number: str | None = None + """Registry registration number.""" + + status: str | None = None + """Normalized registry status of the company (e.g. `active`, `dissolved`, `inactive`, `struck off`).""" + + type: str | None = None + """Legal form as reported by the registry.""" + + risk_level: str | None = None + """Registry-reported risk level, when available. Usually `null` at search time.""" + + fetch_status: str | None = None + """Always `pending` for search candidates - the full company profile is only fetched (and billed) when you select the candidate.""" + + +class KybSearchResponseKybRegistryPagination(BaseModel): + total: int | None = None + """Number of candidates returned.""" + + page: int | None = None + + per_page: int | None = None + + +class KybSearchResponseKybRegistry(BaseModel): + companies: list[KybSearchResponseKybRegistryCompanies] | None = None + """Deduplicated candidate list. Empty while `search_resolved=false` (webhook mode) or when nothing matched.""" + + pagination: KybSearchResponseKybRegistryPagination | None = None + + search_status: Literal["pending", "resolved"] | str | None = None + """Registry search lifecycle status.""" + + search_resolved: bool | None = None + """`true` once the candidate list is final.""" + + +class KybSearchResponse(BaseModel): + request_id: str | None = None + """With `webhook_url`: the persisted search-request id, echoed as `request_id` in the `kyb.registry_search.resolved` callback. Without `webhook_url`: a transient correlation UUID (the search is not stored).""" + + kyb_registry: KybSearchResponseKybRegistry | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/kyb/select_params.py b/src/didit/types/kyb/select_params.py new file mode 100644 index 0000000..0985dc3 --- /dev/null +++ b/src/didit/types/kyb/select_params.py @@ -0,0 +1,22 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["KybSelectParams"] + + +class KybSelectParams(TypedDict, total=False): + kyb_response_id: Required[str] + """Candidate handle from `POST /v3/kyb/search/` (`kyb_registry.companies[].kyb_response_id`). Ephemeral - use it shortly after the search that produced it.""" + + vendor_data: str + """Your identifier for this company; echoed back and stored with the business session.""" + + metadata: Mapping[str, object] | None + """Free-form JSON stored with the business session and echoed back.""" + + save_api_request: bool + """Must be `true` (the default). KYB registry selects are always persisted so billing and retrieval can be tracked; `false` returns `400`.""" diff --git a/src/didit/types/kyb/select_response.py b/src/didit/types/kyb/select_response.py new file mode 100644 index 0000000..ad7d088 --- /dev/null +++ b/src/didit/types/kyb/select_response.py @@ -0,0 +1,349 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.kyb_registry_address import KYBRegistryAddress +from didit.types.components.kyb_registry_industry import KYBRegistryIndustry +from didit.types.components.kyb_registry_ownership import KYBRegistryOwnership +from didit.types.components.kyb_registry_shareholding import KYBRegistryShareholding + +__all__ = [ + "KybSelectResponse", + "KybSelectResponseKybRegistry", + "KybSelectResponseKybRegistryBeneficialOwners", + "KybSelectResponseKybRegistryOfficers", +] + + +class KybSelectResponseKybRegistryBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + """Whether the owner is a natural person or a corporate entity.""" + + roles: list[str] | None = None + """Normalized roles (e.g. `ubo`, `shareholder`).""" + + ownership_min_shares: str | None = None + """Lower bound of the ownership band reported by the registry (e.g. `25%`).""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership band reported by the registry.""" + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + effective_ownership_percent: float | None = None + """Parsed ownership percentage (max bound, falling back to min bound).""" + + company_name: str | None = None + + registration_number: str | None = None + + country_of_birth: str | None = None + + email: str | None = None + + nationality: str | None = None + + designation: str | None = None + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + shareholding: KYBRegistryShareholding | None = None + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + +class KybSelectResponseKybRegistryOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + + designation: str | None = None + """Raw registry designation (e.g. `Director`, `Secretary`).""" + + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "company_officer", + "settlor", + "protector", + "investor", + "other", + ] + | str + | None + ) = None + """Normalized role derived from the designation. Full 17-value model set; registry parsing on this endpoint typically emits director, non_executive_director, secretary, chairman, or company_officer.""" + + nationality: str | None = None + + is_active: bool | None = None + """`false` when the registry marks the appointment as resigned.""" + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + """Status of an associated KYC session, when one was started for this officer. `null` otherwise.""" + + kyc_session_url: str | None = None + """Verification URL of the associated KYC session, when one exists.""" + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + address: str | None = None + """Officer address reported by the registry.""" + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class KybSelectResponseKybRegistry(BaseModel): + """Full registry company profile.""" + + uuid: str | None = None + """Didit's id for this company record.""" + + node_id: str | None = None + + status: Literal["Not Finished", "Approved", "Declined", "In Review"] | str | None = None + """Didit's assessment of the registry check (e.g. `In Review` when the company is not active in the registry or ownership data could not be derived).""" + + registry_status: ( + Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + ) = None + """Normalized company status as reported by the registry.""" + + data_resolved: bool | None = None + """`true` once the registry returned the full profile. `false` when the registry was still processing after polling - the stored record completes automatically once the registry resolves.""" + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """ISO 3166-1 alpha-2.""" + + region: str | None = None + """Subdivision code when the registry is regional (e.g. `CA` for `US-CA`); empty otherwise.""" + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + + tax_number: str | None = None + + risk_level: str | None = None + """Registry-reported risk level, when available.""" + + verification_status: str | None = None + """Registry verification status, when available.""" + + is_from_registry: bool | None = None + """Always `true` for this endpoint - the profile was retrieved from an official registry.""" + + fetch_status: Literal["pending", "resolved"] | str | None = None + """Registry fetch lifecycle. Mirrored by `data_resolved`.""" + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + """Activity description or classification code, when reported.""" + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Decimal amount as a string, when reported.""" + + registered_capital_currency: str | None = None + """ISO 4217 code, when reported.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + """LEI, when reported.""" + + location_of_registration: str | None = None + """Registry/jurisdiction the company is registered with, when reported.""" + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards. At select time this is typically not yet set - VAT and tax ID validation runs when the end user submits the registry step in the hosted flow.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + """Registry-reported share/capital details (currency, total shares, share classes), when available.""" + + officers: list[KybSelectResponseKybRegistryOfficers] | None = None + """Directors, secretaries, and other officers extracted from the registry.""" + + beneficial_owners: list[KybSelectResponseKybRegistryBeneficialOwners] | None = None + """Beneficial owners / persons with significant control extracted from the registry. Can be empty when the registry does not expose ownership.""" + + addresses: list[KYBRegistryAddress] | None = None + """Every address the registry states, one canonical shape. Null when it states none.""" + + industries: list[KYBRegistryIndustry] | None = None + """The activities the registry classifies the company under, one canonical shape. Null when it states none.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead.""" + + user_provided_data: dict[str, object] | None = None + """Always `null` on this endpoint (used by the hosted KYB flow when end users edit registry data).""" + + confirmed_by_user_at: _dt.datetime | None = None + """Always `null` on this endpoint.""" + + last_console_edit_at: _dt.datetime | None = None + """Set when a console reviewer edits the record later.""" + + is_editable: bool | None = None + """Whether the registry-sourced data can still be edited in the hosted flow/console.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """Same as the top-level `registry_tier_delivered`.""" + + registry_ownership_status: Literal["not_requested", "delivered", "unavailable"] | str | None = ( + None + ) + """`delivered` when the ownership data came back, `unavailable` when an ownership tier was requested but the registry had none for this company (see `registry_data.ownership_error`), `not_requested` on the Lite tier.""" + + ownership: KYBRegistryOwnership | None = None + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + registry_tier: str | None = None + """The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`.""" + + +class KybSelectResponse(BaseModel): + request_id: str | None = None + """Id of the business session created for this retrieval. The stored record (visible in the console) keeps updating if the registry resolves after the response.""" + + registry_tier_requested: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier this endpoint sells: `basic` (Lite) for select, `shareholders`, `ubo`.""" + + registry_tier_delivered: Literal["basic", "shareholders", "ubo"] | str | None = None + """The tier actually delivered and billed. Lower than requested when the country's registries hold no ownership data, or the registry has no ownership record for this company.""" + + kyb_registry: KybSelectResponseKybRegistry | None = None + """Full registry company profile.""" + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` you sent.""" + + created_at: _dt.datetime | None = None diff --git a/src/didit/types/networks/__init__.py b/src/didit/types/networks/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/networks/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/networks/list_members_response.py b/src/didit/types/networks/list_members_response.py new file mode 100644 index 0000000..a7b76dd --- /dev/null +++ b/src/didit/types/networks/list_members_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_member import NetworkMember + +__all__ = ["NetworksListMembersResponse"] + + +class NetworksListMembersResponse(BaseModel): + results: list[NetworkMember] | None = None diff --git a/src/didit/types/networks/list_response.py b/src/didit/types/networks/list_response.py new file mode 100644 index 0000000..1e5148c --- /dev/null +++ b/src/didit/types/networks/list_response.py @@ -0,0 +1,35 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_list_item import NetworkListItem + +__all__ = ["NetworksListResponse", "NetworksListResponseKpis"] + + +class NetworksListResponseKpis(BaseModel): + """Each key is {value, delta} over the results matching the current filters.""" + + active: dict[str, object] | None = None + + in_review: dict[str, object] | None = None + + applicants_involved: dict[str, object] | None = None + + detected_this_period: dict[str, object] | None = None + + false_positive_rate: dict[str, object] | None = None + + +class NetworksListResponse(BaseModel): + count: int | None = None + + next: int | None = None + """Offset of the next page, or null on the last page.""" + + previous: int | None = None + + results: list[NetworkListItem] | None = None + + kpis: NetworksListResponseKpis | None = None + """Each key is {value, delta} over the results matching the current filters.""" diff --git a/src/didit/types/networks/list_signals_response.py b/src/didit/types/networks/list_signals_response.py new file mode 100644 index 0000000..35563c8 --- /dev/null +++ b/src/didit/types/networks/list_signals_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_signal import NetworkSignal + +__all__ = ["NetworksListSignalsResponse"] + + +class NetworksListSignalsResponse(BaseModel): + results: list[NetworkSignal] | None = None diff --git a/src/didit/types/networks/notes/__init__.py b/src/didit/types/networks/notes/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/networks/notes/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/networks/notes/create_params.py b/src/didit/types/networks/notes/create_params.py new file mode 100644 index 0000000..240bb3e --- /dev/null +++ b/src/didit/types/networks/notes/create_params.py @@ -0,0 +1,12 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["NetworksNotesCreateParams"] + + +class NetworksNotesCreateParams(TypedDict, total=False): + comment: Required[str] + + client_reference: Required[str] diff --git a/src/didit/types/networks/notes/list_response.py b/src/didit/types/networks/notes/list_response.py new file mode 100644 index 0000000..7bda8c3 --- /dev/null +++ b/src/didit/types/networks/notes/list_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_note import NetworkNote + +__all__ = ["NetworksNotesListResponse"] + + +class NetworksNotesListResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[NetworkNote] | None = None diff --git a/src/didit/types/networks/retrieve_timeline_response.py b/src/didit/types/networks/retrieve_timeline_response.py new file mode 100644 index 0000000..6fbf7c5 --- /dev/null +++ b/src/didit/types/networks/retrieve_timeline_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.network_timeline_event import NetworkTimelineEvent + +__all__ = ["NetworksRetrieveTimelineResponse"] + + +class NetworksRetrieveTimelineResponse(BaseModel): + results: list[NetworkTimelineEvent] | None = None diff --git a/src/didit/types/passive_liveness/__init__.py b/src/didit/types/passive_liveness/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/passive_liveness/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/passive_liveness/check_params.py b/src/didit/types/passive_liveness/check_params.py new file mode 100644 index 0000000..502a67e --- /dev/null +++ b/src/didit/types/passive_liveness/check_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["PassiveLivenessCheckParams"] + + +class PassiveLivenessCheckParams(TypedDict, total=False): + user_image: Required[FileTypes] + """Front-facing face image (e.g. a selfie). Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `webp`. Maximum upload size: **5 MB** (larger files are rejected with `400`). Images are automatically compressed to ~0.5 MB before processing, so very high resolutions do not improve accuracy. The image should contain a single subject - when several faces are present, the largest one is evaluated and a `MULTIPLE_FACES_DETECTED` warning is added.""" + + face_liveness_score_decline_threshold: float + """Liveness score threshold (0–100). A computed score **at or below** this value emits a `LOW_LIVENESS_SCORE` warning and sets `status` to `Declined`. Default `30`; raise it for a stricter anti-spoofing posture. Values outside 0–100 return `400`.""" + + rotate_image: bool + """When `true`, the service tries 90-degree rotations of the input and uses the orientation that yields the best face detection. Useful when EXIF orientation is missing. Adds latency.""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session, stores the face images, emits a `status.updated` webhook, and enrolls the detected face into your face search index. When `false`, the call is one-shot - nothing is stored and the face is not enrolled. Blocklist and duplicate screening run either way.""" + + vendor_data: str + """Optional opaque identifier (your user id, email, UUID…) stored on the persisted session and echoed back. Also used to exclude prior sessions with the same `vendor_data` from the duplicate-face check, so re-verifying the same user does not raise `DUPLICATED_FACE`.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back. In multipart requests, send it as a JSON-encoded string field - it is parsed into an object.""" diff --git a/src/didit/types/passive_liveness/check_response.py b/src/didit/types/passive_liveness/check_response.py new file mode 100644 index 0000000..7419e87 --- /dev/null +++ b/src/didit/types/passive_liveness/check_response.py @@ -0,0 +1,114 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "PassiveLivenessCheckResponse", + "PassiveLivenessCheckResponseLiveness", + "PassiveLivenessCheckResponseLivenessUserImage", + "PassiveLivenessCheckResponseLivenessUserImageEntities", + "PassiveLivenessCheckResponseLivenessWarnings", +] + + +class PassiveLivenessCheckResponseLivenessUserImageEntities(BaseModel): + bbox: list[int] | None = None + """Bounding box of the detected face as `[x_min, y_min, x_max, y_max]` pixel coordinates in the processed image.""" + + confidence: float | None = None + """Face-detection confidence (0–1).""" + + age: float | None = None + """Model-estimated age of the detected face, in years. Informational only - it does not affect the liveness decision.""" + + gender: str | None = None + """Model-predicted gender of the detected face (`male` or `female`). Informational only.""" + + race: str | None = None + """Reserved field - always `null` in responses from this endpoint.""" + + +class PassiveLivenessCheckResponseLivenessUserImage(BaseModel): + """Face-detection results for the submitted image.""" + + entities: list[PassiveLivenessCheckResponseLivenessUserImageEntities] | None = None + """One entry per detected face. The largest face is the one evaluated.""" + + best_angle: int | None = None + """Rotation (degrees) that produced the best face detection; non-zero only when `rotate_image=true` corrected the orientation.""" + + +class PassiveLivenessCheckResponseLivenessWarnings(BaseModel): + risk: ( + Literal[ + "NO_FACE_DETECTED", + "LOW_LIVENESS_SCORE", + "LIVENESS_FACE_ATTACK", + "MULTIPLE_FACES_DETECTED", + "FACE_IN_BLOCKLIST", + "POSSIBLE_FACE_IN_BLOCKLIST", + "DUPLICATED_FACE", + "POSSIBLE_DUPLICATED_FACE", + ] + | str + | None + ) = None + """Machine-readable risk code.""" + + feature: Literal["LIVENESS"] | str | None = None + """Feature that raised the warning. Always `LIVENESS` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """`null` for most warnings. Blocklist hits carry `{blocklisted_session_id, blocklisted_session_number, api_service}`; duplicate hits carry `{duplicated_session_id, duplicated_session_number, api_service}` pointing at the matching session.""" + + log_type: Literal["error", "warning", "information"] | str | None = None + """Severity. `error` warnings drive `status` to `Declined`; `warning` and `information` entries are advisory and never decline on their own.""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class PassiveLivenessCheckResponseLiveness(BaseModel): + status: Literal["Approved", "Declined"] | str | None = None + """`Declined` when at least one `error`-severity warning fired (no face, low score, presentation attack, or a blocklist hit); `Approved` otherwise. `warning`/`information` entries (multiple faces, duplicates) never decline on their own.""" + + method: Literal["PASSIVE"] | str | None = None + """Always `PASSIVE` for this endpoint.""" + + score: float | None = None + """Liveness confidence (0–100, two decimals) that the image depicts a live person rather than a spoof. `null` when the model returned no confidence.""" + + user_image: PassiveLivenessCheckResponseLivenessUserImage | None = None + """Face-detection results for the submitted image.""" + + warnings: list[PassiveLivenessCheckResponseLivenessWarnings] | None = None + """Risk signals. `error` severity (declines): `NO_FACE_DETECTED`, `LOW_LIVENESS_SCORE` (score at or below the threshold), `LIVENESS_FACE_ATTACK`, `FACE_IN_BLOCKLIST`, `POSSIBLE_FACE_IN_BLOCKLIST`. `warning` severity (review, no decline): `MULTIPLE_FACES_DETECTED`. `information` severity (no decline): `DUPLICATED_FACE`, `POSSIBLE_DUPLICATED_FACE` - the same face already appeared in another approved session.""" + + face_quality: float | None = None + """Capture-quality score of the detected face, normalized to 0–100. `null` when the model could not produce a reliable value.""" + + face_luminance: float | None = None + """Brightness of the detected face, normalized to 0–100. `null` when unavailable.""" + + +class PassiveLivenessCheckResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + liveness: PassiveLivenessCheckResponseLiveness | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """ISO 8601 timestamp (UTC) of when the response was generated, e.g. `2026-06-12T01:04:42.763237+00:00`.""" diff --git a/src/didit/types/phone/__init__.py b/src/didit/types/phone/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/phone/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/phone/assess_risk_params.py b/src/didit/types/phone/assess_risk_params.py new file mode 100644 index 0000000..aba8898 --- /dev/null +++ b/src/didit/types/phone/assess_risk_params.py @@ -0,0 +1,30 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +from didit.types.components.risk_signals import RiskSignals + +__all__ = ["PhoneAssessRiskParams"] + + +class PhoneAssessRiskParams(TypedDict, total=False): + phone_number: Required[str] + """Phone number to score. It is validated and normalized to E.164.""" + + country_code: str + """Optional ISO 3166-1 alpha-2 or alpha-3 country hint for intelligence coverage.""" + + signals: RiskSignals + """Optional device and network context observed by your application. All fields are optional. Passing them feeds cross-session match detection in Networks.""" + + vendor_data: str + """Your stable reference for this request. Stored on the API session and echoed in the response.""" + + metadata: Mapping[str, object] | None + """Your non-sensitive JSON metadata. Stored on the API session and echoed in the response.""" + + enable_social: bool + """When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price.""" diff --git a/src/didit/types/phone/check_code_params.py b/src/didit/types/phone/check_code_params.py new file mode 100644 index 0000000..48a48ed --- /dev/null +++ b/src/didit/types/phone/check_code_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["PhoneCheckCodeParams"] + + +class PhoneCheckCodeParams(TypedDict, total=False): + phone_number: Required[str] + """The same phone number used in the matching [`POST /v3/phone/send/`](/standalone-apis/phone-send) call, in E.164 format. This is what links the check to the send.""" + + code: Required[str] + """The OTP the end user received. Must be 4–8 numeric digits - anything else returns `400` without consuming an attempt. A well-formed but wrong code returns `200` with `status: "Failed"` and consumes one of the 3 attempts.""" + + duplicated_phone_number_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the same number was already used by a different user (different `vendor_data`) of your application. `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings` and fills `phone.matches`.""" + + disposable_number_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the carrier lookup flags the number as a temporary/burner line (`phone.is_disposable`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`.""" + + voip_number_action: Literal["NO_ACTION", "DECLINE"] | str + """What to do when the carrier lookup reports a virtual line type - `voip`, `isp`, or `vpn` (`phone.is_virtual`). `DECLINE` flips the final `status` to `Declined`; `NO_ACTION` records the risk in `phone.warnings`.""" + + enable_social: bool + """When true, also look up the social footprint of the number - the messaging, social, e-commerce and professional platforms it is registered on. Billed as one `phone_social` unit per request in addition to this endpoint's own price.""" + + phone_no_social_presence_action: Literal["NO_ACTION", "DECLINE"] | str + """Action when the social footprint check finds the number registered on none of the platforms it covers. Only NO_ACTION and DECLINE are accepted on this endpoint; REVIEW is available on the workflow configuration key of the same name.""" diff --git a/src/didit/types/phone/send_code_params.py b/src/didit/types/phone/send_code_params.py new file mode 100644 index 0000000..7650a74 --- /dev/null +++ b/src/didit/types/phone/send_code_params.py @@ -0,0 +1,69 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["PhoneSendCodeParams", "PhoneSendCodeParamsOptions", "PhoneSendCodeParamsSignals"] + + +class PhoneSendCodeParamsOptions(TypedDict, total=False): + """OTP delivery options. All fields are optional.""" + + code_size: int + """Number of digits in the OTP.""" + + locale: str + """BCP-47 locale used to localize the verification message (e.g., `en-US`, `fr`, `es`). Invalid formats return `400`.""" + + preferred_channel: Literal["whatsapp", "sms", "telegram", "voice", "rcs", "viber", "zalo"] | str + """Preferred delivery channel. If the channel is unavailable for the destination country or number, the message automatically falls back to SMS - the send still reports `Success`.""" + + use_white_label_customization: bool + """When `true`, the send is white-labelled: if your application has no SMS sender name, the SMS names your white-label public name instead ("123456 is your verification code for Acme Bank"). Your SMS sender name, when set, is used with or without this flag. Applies to SMS messages, including an SMS fallback from another channel; WhatsApp codes are sent with a fixed message and are not branded. Defaults to `false`.""" + + sms_sender_name: str | None + """The name this code's SMS carries ("123456 is your verification code for Acme Bank"), instead of your application's SMS sender name. Free. 1 to 30 characters: letters, digits, spaces and ' & - ! only, not only digits, with single spaces between words; any other value returns `400` and no code is sent. Omitted, `null` or blank, your application's SMS sender name is used. Each application can start using up to two new names per day (UTC); past that, the code is sent with your application's name. Applies to SMS messages, including an SMS fallback from another channel; WhatsApp codes are not branded.""" + + +class PhoneSendCodeParamsSignals(TypedDict, total=False): + """Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.""" + + ip: str + """IP address of the end user's device. IPv4 or IPv6.""" + + device_id: str + """Unique identifier of the end user's device. Android: `ANDROID_ID`; iOS: `identifierForVendor`.""" + + device_platform: Literal["android", "ios", "ipados", "tvos", "web"] | str + """Platform of the end user's device.""" + + device_model: str + """Model of the end user's device.""" + + os_version: str + """Operating-system version of the end user's device.""" + + app_version: str + """Version of your application.""" + + user_agent: str + """User agent of the end user's browser or app.""" + + +class PhoneSendCodeParams(TypedDict, total=False): + phone_number: Required[str] + """Recipient phone number in E.164 format - leading `+` and country code (e.g., `+14155552671`). Numbers are validated and normalized to E.164; unparseable or invalid numbers return `400` with a `phone_number` field error.""" + + options: PhoneSendCodeParamsOptions + """OTP delivery options. All fields are optional.""" + + signals: PhoneSendCodeParamsSignals + """Optional device and network signals about the end user, forwarded to the anti-fraud layer to improve detection of abusive or automated traffic. All fields are optional.""" + + vendor_data: str + """Optional caller-controlled identifier (your internal user id, an email, a UUID, etc.) persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console. Use it to correlate Didit's `request_id` with your user record.""" + + metadata: Mapping[str, object] | None + """Optional free-form JSON object persisted on the session and echoed back in the send response, the matching check response, webhooks, and the Business Console.""" diff --git a/src/didit/types/proof_of_address/__init__.py b/src/didit/types/proof_of_address/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/proof_of_address/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/proof_of_address/verify_params.py b/src/didit/types/proof_of_address/verify_params.py new file mode 100644 index 0000000..43d9192 --- /dev/null +++ b/src/didit/types/proof_of_address/verify_params.py @@ -0,0 +1,63 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +from didit._types import FileTypes + +__all__ = ["ProofOfAddressVerifyParams"] + + +class ProofOfAddressVerifyParams(TypedDict, total=False): + document: Required[FileTypes] + """The Proof of Address document. Allowed extensions: `tiff`, `jpg`, `jpeg`, `png`, `pdf`, `webp`. Maximum upload size: **15 MB** (larger files are rejected with `400`). Multi-page PDFs are supported (expect higher latency); encrypted PDFs require `document_password`. Images are automatically compressed before processing.""" + + document_password: str + """Password to decrypt `document` when it is an encrypted PDF. Sending an encrypted PDF without (or with a wrong) password returns `400`.""" + + expected_address: str + """Address to verify against the document. It is geocoded and compared with the extracted address; a mismatch (or an expected address that cannot be geocoded/verified) adds `ADDRESS_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`. The geocoded form is echoed back in `expected_details_formatted_address` / `expected_details_parsed_address`.""" + + expected_country: str + """Country to verify against the document (ISO 3166-1 code, e.g. `ARG` or `AR`). A mismatch with the document's country adds `POA_COUNTRY_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`.""" + + expected_first_name: str + """First name to verify against `name_on_document` (fuzzy match, transliteration-aware). A score below the match threshold adds `NAME_MISMATCH_WITH_PROVIDED`, governed by `poa_address_mismatch_action`.""" + + expected_last_name: str + """Last name to verify against `name_on_document`, combined with `expected_first_name` for the match score.""" + + poa_languages_allowed: str + """Comma-separated list of accepted document languages as ISO 639-1 codes (e.g. `en,es,fr`). When omitted or blank, **all 51 supported languages** are accepted. A document in a language outside the list adds `UNSUPPORTED_DOCUMENT_LANGUAGE` (governed by `poa_unsupported_language_action`); an unknown code in the list returns `400` with the full set of supported codes.""" + + poa_document_age_months: str + """Comma-separated `key:value` pairs setting the maximum document age in months per document type - keys: `utility_bill`, `bank_statement`, `government_issued_document`, `other_poa_document`; values: `1`–`120`, or `-1` for unlimited (no age check). Defaults when omitted: `utility_bill:3,bank_statement:3,government_issued_document:12,other_poa_document:12`. A document older than its limit declines with `POA_DOCUMENT_EXPIRED`. **Caution:** when you provide this field, list *every* type you accept - omitted types are treated as not accepted (informational `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`) and their age check is skipped.""" + + poa_name_mismatch_action: Literal["DECLINE", "NO_ACTION"] | str + """Accepted but **currently not applied** - name-mismatch warnings (`NAME_MISMATCH_WITH_PROVIDED`) follow `poa_address_mismatch_action` instead. Set `poa_address_mismatch_action` to control both name and address mismatch behavior.""" + + poa_document_issues_action: Literal["DECLINE", "NO_ACTION"] | str + """What to do on file-integrity problems - in practice only `DOCUMENT_METADATA_MISMATCH` is produced by this endpoint.""" + + poa_document_authenticity_action: Literal["DECLINE", "NO_ACTION"] | str + """What to do when document manipulation is suspected (`SUSPECTED_DOCUMENT_MANIPULATION`) - e.g. the PDF was modified after digital signing, processed by a known PDF editor, shows overlay-text manipulation, or has inconsistent EXIF dates.""" + + poa_unsupported_language_action: Literal["DECLINE", "NO_ACTION"] | str + """What to do when the document language is outside `poa_languages_allowed` (`UNSUPPORTED_DOCUMENT_LANGUAGE`).""" + + poa_address_mismatch_action: Literal["DECLINE", "NO_ACTION"] | str + """What to do when the extracted details disagree with the `expected_*` fields: `ADDRESS_MISMATCH_WITH_PROVIDED`, `NAME_MISMATCH_WITH_PROVIDED`, and `POA_COUNTRY_MISMATCH_WITH_PROVIDED` all follow this action.""" + + poa_issuer_not_identified_action: Literal["DECLINE", "NO_ACTION"] | str + """What to do when no issuing company/organization can be identified on the document (`ISSUER_NOT_IDENTIFIED`).""" + + save_api_request: bool + """When `true` (default), persists the call as an API-type session - visible in the Business Console, retrievable via `GET /v3/session/{sessionId}/decision/` using the returned `request_id`, announced through a `status.updated` webhook, and with the uploaded document stored. When `false`, nothing is stored and `request_id` is a transient UUID for response correlation only.""" + + vendor_data: str + """Optional opaque string (your internal user id, email, UUID…) stored on the persisted session and echoed back in the response. Use it to correlate API calls with your own records and to filter sessions later.""" + + metadata: Mapping[str, object] + """Optional JSON object stored with the session (when `save_api_request=true`) and echoed back in the response. In multipart requests, send it as a JSON-encoded string field (e.g. `metadata={"flow":"onboarding"}`) - it is parsed into an object.""" diff --git a/src/didit/types/proof_of_address/verify_response.py b/src/didit/types/proof_of_address/verify_response.py new file mode 100644 index 0000000..a0d76f5 --- /dev/null +++ b/src/didit/types/proof_of_address/verify_response.py @@ -0,0 +1,171 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.document_ai_detected_code import DocumentAIDetectedCode + +__all__ = [ + "ProofOfAddressVerifyResponse", + "ProofOfAddressVerifyResponsePoa", + "ProofOfAddressVerifyResponsePoaExtraFields", + "ProofOfAddressVerifyResponsePoaPoaParsedAddress", + "ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation", + "ProofOfAddressVerifyResponsePoaWarnings", +] + + +class ProofOfAddressVerifyResponsePoaExtraFields(BaseModel): + """Document-specific extras. Bank fields are populated for bank statements when printed on the document; `null` otherwise.""" + + bank_account_number: str | None = None + + bank_iban: str | None = None + + bank_sort_code: str | None = None + + bank_routing_number: str | None = None + + bank_swift_bic: str | None = None + + bank_branch_name: str | None = None + + bank_branch_address: str | None = None + + document_phone_number: str | None = None + """Contact phone number printed on the document.""" + + additional_names: list[str] | None = None + """Other holder names found on the document (e.g. co-account holders, previous account names).""" + + +class ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation(BaseModel): + """Geocoded coordinates of the document address.""" + + latitude: float | None = None + + longitude: float | None = None + + +class ProofOfAddressVerifyResponsePoaPoaParsedAddress(BaseModel): + """Structured, geocoded breakdown of the document address. `null` when the address could not be parsed.""" + + street_1: str | None = None + + street_2: str | None = None + + city: str | None = None + + region: str | None = None + + country: str | None = None + """ISO 3166-1 alpha-2 country code.""" + + postal_code: str | None = None + + document_location: ProofOfAddressVerifyResponsePoaPoaParsedAddressDocumentLocation | None = None + """Geocoded coordinates of the document address.""" + + +class ProofOfAddressVerifyResponsePoaWarnings(BaseModel): + risk: str | None = None + """Machine-readable risk code. Always-decline: `MISSING_ADDRESS_INFORMATION`, `POA_DOCUMENT_EXPIRED`, `INVALID_DOCUMENT_TYPE`, `UNABLE_TO_VALIDATE_DOCUMENT_AGE`, plus `UNABLE_TO_EXTRACT_ISSUE_DATE` and `POA_NAME_NOT_DETECTED` (fixed to decline on this endpoint). Governed by request options: `NAME_MISMATCH_WITH_PROVIDED` / `ADDRESS_MISMATCH_WITH_PROVIDED` / `POA_COUNTRY_MISMATCH_WITH_PROVIDED` (`poa_address_mismatch_action`), `POOR_DOCUMENT_QUALITY` (never produced by this endpoint) / `DOCUMENT_METADATA_MISMATCH` (`poa_document_issues_action`), `SUSPECTED_DOCUMENT_MANIPULATION` (`poa_document_authenticity_action`), `UNSUPPORTED_DOCUMENT_LANGUAGE` (`poa_unsupported_language_action`), `ISSUER_NOT_IDENTIFIED` (`poa_issuer_not_identified_action`). Informational on this endpoint: `POA_DOCUMENT_NOT_SUPPORTED_FOR_APPLICATION`, `UNPARSABLE_OR_INVALID_ADDRESS`.""" + + feature: Literal["PROOF_OF_ADDRESS"] | str | None = None + """Feature that raised the warning. Always `PROOF_OF_ADDRESS` on this endpoint.""" + + additional_data: dict[str, object] | None = None + """Extra context for some risks - e.g. `POA_DOCUMENT_EXPIRED` includes `expiration_reason` (`printed_expiration` or `age_window`), `document_type`, `document_subtype`, and `issue_date`, plus `expiration_date` on the `printed_expiration` path or `max_age_months` on the `age_window` path; `SUSPECTED_DOCUMENT_MANIPULATION` includes `reason` and `detection_method`. `null` for most risks.""" + + log_type: Literal["error", "information"] | str | None = None + """Severity. `error` warnings drive `status` to `Declined`; `information` entries are advisory and never decline on their own. (`warning` is not produced by this endpoint - its options only accept `DECLINE` or `NO_ACTION`.)""" + + short_description: str | None = None + """Human-readable one-line summary of the risk.""" + + long_description: str | None = None + """Human-readable explanation of the risk.""" + + +class ProofOfAddressVerifyResponsePoa(BaseModel): + status: Literal["Approved", "Declined"] | str | None = None + """`Approved` when no warning resolves to a decline; `Declined` otherwise - the `warnings` list explains why.""" + + issuing_state: str | None = None + """Country the document was issued in, as extracted from the document - usually ISO 3166-1 alpha-3 (e.g. `ARG`).""" + + document_type: ( + Literal[ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN", + ] + | str + | None + ) = None + """Detected document category. `UNKNOWN` means the document could not be classified as an acceptable Proof of Address and auto-declines with `INVALID_DOCUMENT_TYPE`. Never null - classification failures fall back to `UNKNOWN`.""" + + document_subtype: str | None = None + """Finer-grained classification within `document_type` - e.g. `ELECTRICITY_BILL`, `WATER_BILL`, `GAS_BILL`, `INTERNET_BILL`, `PHONE_BILL` (utility bills); `ACCOUNT_STATEMENT`, `CREDIT_CARD_STATEMENT` (bank statements); `TAX_ASSESSMENT`, `RESIDENCY_CERTIFICATE` (government documents). `UNKNOWN` when no subtype could be determined. Never null - classification failures fall back to `UNKNOWN`.""" + + document_language: str | None = None + """Detected document language (ISO 639-1).""" + + issuer: str | None = None + """Company or organization that issued the document. `null` adds `ISSUER_NOT_IDENTIFIED`.""" + + issue_date: _dt.date | None = None + """Document issue date (`YYYY-MM-DD`). `null` when it could not be extracted - that declines with `UNABLE_TO_EXTRACT_ISSUE_DATE` on this endpoint.""" + + expiration_date: _dt.date | None = None + """Explicit expiry/validity date printed on the document, when present. Independent of the `poa_document_age_months` age check.""" + + poa_address: str | None = None + """Address exactly as printed on the document. `null` auto-declines with `MISSING_ADDRESS_INFORMATION`.""" + + poa_formatted_address: str | None = None + """Geocoded, normalized version of `poa_address`.""" + + poa_parsed_address: ProofOfAddressVerifyResponsePoaPoaParsedAddress | None = None + """Structured, geocoded breakdown of the document address. `null` when the address could not be parsed.""" + + expected_details_address: str | None = None + """Echo of the `expected_address` you sent, or `null`.""" + + expected_details_formatted_address: str | None = None + """Geocoded, normalized version of `expected_address`; `null` when not provided or not geocodable.""" + + expected_details_parsed_address: dict[str, object] | None = None + """Full geocoding result for `expected_address` (street/city/region/country/postal code plus raw geocoder output and an `is_verified` flag). `null` when `expected_address` was not provided.""" + + name_on_document: str | None = None + """Primary holder name extracted from the document; compared against `expected_first_name`/`expected_last_name` when provided. `null` declines with `POA_NAME_NOT_DETECTED` on this endpoint.""" + + extra_fields: ProofOfAddressVerifyResponsePoaExtraFields | None = None + """Document-specific extras. Bank fields are populated for bank statements when printed on the document; `null` otherwise.""" + + warnings: list[ProofOfAddressVerifyResponsePoaWarnings] | None = None + """Empty on a clean approval. Every entry explains one detected risk; entries with `log_type: "error"` set `status` to `Declined`.""" + + detected_codes: list[DocumentAIDetectedCode] | None = None + """QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.""" + + +class ProofOfAddressVerifyResponse(BaseModel): + request_id: str | None = None + """Persisted session id when `save_api_request=true` (usable with `GET /v3/session/{sessionId}/decision/`); otherwise a transient correlation UUID.""" + + poa: ProofOfAddressVerifyResponsePoa | None = None + + vendor_data: str | None = None + """Echo of the `vendor_data` you sent, or `null`.""" + + metadata: dict[str, object] | None = None + """Echo of the `metadata` object you sent, or `null`.""" + + created_at: _dt.datetime | None = None + """ISO 8601 timestamp (UTC) of when the response was generated.""" diff --git a/src/didit/types/questionnaires/__init__.py b/src/didit/types/questionnaires/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/questionnaires/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/questionnaires/create_params.py b/src/didit/types/questionnaires/create_params.py new file mode 100644 index 0000000..c10447a --- /dev/null +++ b/src/didit/types/questionnaires/create_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.simple_questionnaire_element import SimpleQuestionnaireElement + +__all__ = ["QuestionnairesCreateParams"] + + +class QuestionnairesCreateParams(TypedDict, total=False): + """Create a simple linear questionnaire. The API converts `form_elements` into the internal questionnaire graph.""" + + title: Required[str] + """Internal name for the questionnaire.""" + + description: str | None + + languages: Required[Sequence[str]] + """Languages included in every label. Must include `en`.""" + + default_language: str + """Default language. Must be included in `languages`.""" + + is_active: bool + + status: Literal["draft", "published"] | str + """Omit this field to create a published questionnaire ready for workflows.""" + + form_elements: Required[Sequence[SimpleQuestionnaireElement]] + """Questions in the exact order users should see them. Branching and conditional fields are not supported.""" diff --git a/src/didit/types/questionnaires/list_response.py b/src/didit/types/questionnaires/list_response.py new file mode 100644 index 0000000..779a306 --- /dev/null +++ b/src/didit/types/questionnaires/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.questionnaire_list_item import QuestionnaireListItem + +__all__ = ["QuestionnairesListResponse"] + + +class QuestionnairesListResponse(BaseModel): + count: int | None = None + """Total number of questionnaire versions for the application.""" + + next: str | None = None + """URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """URL of the previous page, or `null` on the first page.""" + + results: list[QuestionnaireListItem] | None = None diff --git a/src/didit/types/questionnaires/update_params.py b/src/didit/types/questionnaires/update_params.py new file mode 100644 index 0000000..c5bb658 --- /dev/null +++ b/src/didit/types/questionnaires/update_params.py @@ -0,0 +1,29 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit.types.components.simple_questionnaire_element import SimpleQuestionnaireElement + +__all__ = ["QuestionnairesUpdateParams"] + + +class QuestionnairesUpdateParams(TypedDict, total=False): + """Update questionnaire metadata or replace the full linear `form_elements` list.""" + + title: str + + description: str | None + + languages: Sequence[str] + + default_language: str + + is_active: bool + + status: Literal["draft", "published"] | str + + form_elements: Sequence[SimpleQuestionnaireElement] + """If provided, this replaces the full linear question list.""" diff --git a/src/didit/types/regulatory_reports/__init__.py b/src/didit/types/regulatory_reports/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/regulatory_reports/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/regulatory_reports/create_params.py b/src/didit/types/regulatory_reports/create_params.py new file mode 100644 index 0000000..03b80f8 --- /dev/null +++ b/src/didit/types/regulatory_reports/create_params.py @@ -0,0 +1,68 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["RegulatoryReportsCreateParams", "RegulatoryReportsCreateParamsNarrative"] + + +class RegulatoryReportsCreateParamsNarrative(TypedDict, total=False): + who: str + + what: str + + when: str + + where: str + + why: str + + how: str + + +class RegulatoryReportsCreateParams(TypedDict, total=False): + case: Required[str] + + template: Required[str] + + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str + """Required unless a template is given; the template's report_type is used otherwise.""" + + jurisdiction: str + """Required unless a template is given.""" + + transactions: Sequence[str] | None + """Transactions to attach; defaults to the case's linked transactions.""" + + rentity_id: str | None + + rentity_branch: str | None + + submission_code: Literal["E", "M"] | str + + entity_reference: str | None + + fiu_ref_number: str | None + + currency_code_local: str | None + + submission_date: str | _dt.datetime | None + + reporting_person: Mapping[str, object] + + location: Mapping[str, object] + + reason: str | None + + action: str | None + + activity: str | None + + narrative: RegulatoryReportsCreateParamsNarrative + + report_indicators: Sequence[str] + + included_fields: Sequence[str] diff --git a/src/didit/types/regulatory_reports/list_response.py b/src/didit/types/regulatory_reports/list_response.py new file mode 100644 index 0000000..c6e6585 --- /dev/null +++ b/src/didit/types/regulatory_reports/list_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.regulatory_report_detail import RegulatoryReportDetail + +__all__ = ["RegulatoryReportsListResponse"] + + +class RegulatoryReportsListResponse(BaseModel): + count: int | None = None + """Number of matching rows, capped at 100 for performance.""" + + next: str | None = None + + previous: str | None = None + + results: list[RegulatoryReportDetail] | None = None diff --git a/src/didit/types/regulatory_reports/retrieve_download_url_response.py b/src/didit/types/regulatory_reports/retrieve_download_url_response.py new file mode 100644 index 0000000..68ae2bf --- /dev/null +++ b/src/didit/types/regulatory_reports/retrieve_download_url_response.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["RegulatoryReportsRetrieveDownloadUrlResponse"] + + +class RegulatoryReportsRetrieveDownloadUrlResponse(BaseModel): + download_url: str | None = None + + file: Literal["pdf", "xml"] | str | None = None diff --git a/src/didit/types/regulatory_reports/update_params.py b/src/didit/types/regulatory_reports/update_params.py new file mode 100644 index 0000000..c88de3c --- /dev/null +++ b/src/didit/types/regulatory_reports/update_params.py @@ -0,0 +1,57 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import TypedDict + +__all__ = ["RegulatoryReportsUpdateParams", "RegulatoryReportsUpdateParamsNarrative"] + + +class RegulatoryReportsUpdateParamsNarrative(TypedDict, total=False): + who: str + + what: str + + when: str + + where: str + + why: str + + how: str + + +class RegulatoryReportsUpdateParams(TypedDict, total=False): + jurisdiction: str + + rentity_id: str | None + + rentity_branch: str | None + + submission_code: Literal["E", "M"] | str + + entity_reference: str | None + + fiu_ref_number: str | None + + currency_code_local: str | None + + submission_date: str | _dt.datetime | None + + reporting_person: Mapping[str, object] + + location: Mapping[str, object] + + reason: str | None + + action: str | None + + activity: str | None + + narrative: RegulatoryReportsUpdateParamsNarrative + + report_indicators: Sequence[str] + + included_fields: Sequence[str] diff --git a/src/didit/types/regulatory_reports/validate_response.py b/src/didit/types/regulatory_reports/validate_response.py new file mode 100644 index 0000000..c288363 --- /dev/null +++ b/src/didit/types/regulatory_reports/validate_response.py @@ -0,0 +1,23 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["RegulatoryReportsValidateResponse", "RegulatoryReportsValidateResponseErrors"] + + +class RegulatoryReportsValidateResponseErrors(BaseModel): + field: str | None = None + + message: str | None = None + + +class RegulatoryReportsValidateResponse(BaseModel): + status: Literal["DRAFT", "VALIDATED", "FINALIZED"] | str | None = None + + is_valid: bool | None = None + + errors: list[RegulatoryReportsValidateResponseErrors] | None = None + """Empty when valid.""" diff --git a/src/didit/types/report_templates/__init__.py b/src/didit/types/report_templates/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/report_templates/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/report_templates/create_params.py b/src/didit/types/report_templates/create_params.py new file mode 100644 index 0000000..8638475 --- /dev/null +++ b/src/didit/types/report_templates/create_params.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["ReportTemplatesCreateParams"] + + +class ReportTemplatesCreateParams(TypedDict, total=False): + name: Required[str] + + jurisdiction: Required[str] + """ISO 3166-1 alpha-2 country code, upper-cased on save.""" + + report_type: Required[Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str] + + field_config: Mapping[str, object] + """Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template.""" diff --git a/src/didit/types/report_templates/list_response.py b/src/didit/types/report_templates/list_response.py new file mode 100644 index 0000000..dc93048 --- /dev/null +++ b/src/didit/types/report_templates/list_response.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.report_template_detail import ReportTemplateDetail + +__all__ = ["ReportTemplatesListResponse"] + + +class ReportTemplatesListResponse(BaseModel): + count: int | None = None + """Number of matching rows, capped at 100 for performance.""" + + next: str | None = None + + previous: str | None = None + + results: list[ReportTemplateDetail] | None = None diff --git a/src/didit/types/report_templates/update_params.py b/src/didit/types/report_templates/update_params.py new file mode 100644 index 0000000..1a4d02e --- /dev/null +++ b/src/didit/types/report_templates/update_params.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping + +from typing_extensions import TypedDict + +__all__ = ["ReportTemplatesUpdateParams"] + + +class ReportTemplatesUpdateParams(TypedDict, total=False): + name: str + + jurisdiction: str + """ISO 3166-1 alpha-2 country code, upper-cased on save.""" + + report_type: Literal["SAR", "STR", "CTR", "TTR", "UTR", "IFT", "CBR", "TFR"] | str + + field_config: Mapping[str, object] + """Reusable goAML header defaults. Template-level obligatory fields: rentity_id, submission_code, and currency_code_local (3-letter). Optional: enabled_fields (which optional report blocks to include) plus reporting_person, location, reason, and action defaults. Per-filing header fields (action, submission_date, transactions, jurisdiction) are enforced when the report is validated or finalized, not on the template.""" diff --git a/src/didit/types/sessions/__init__.py b/src/didit/types/sessions/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/sessions/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/sessions/batch_delete_params.py b/src/didit/types/sessions/batch_delete_params.py new file mode 100644 index 0000000..c242502 --- /dev/null +++ b/src/didit/types/sessions/batch_delete_params.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +__all__ = ["SessionsBatchDeleteParams"] + + +class SessionsBatchDeleteParams(TypedDict, total=False): + session_numbers: Sequence[str] + """Numeric `session_number` values of the KYC sessions to delete, as digit-only strings (JSON numbers are also accepted and coerced). Required unless `delete_all` is `true`; an empty list returns `400`. Any non-digit entry rejects the whole request with `400` (unless `delete_all` is `true`, in which case validation is skipped and the entries are ignored). Numbers that do not match a live session in your application are silently skipped.""" + + delete_all: bool + """When `true`, every remaining KYC session in the calling application is deleted and `session_numbers` is ignored. More than 20 matching sessions run as a deletion job (`202`) that covers only the sessions created before the request. Defaults to `false`. Irreversible; there is no confirmation step.""" + + retain_face_embeddings: bool | None + """Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.""" + + face_retention_days: int + """Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`.""" + + face_retention_deadline: str | _dt.datetime + """Optional hard expiry for a retained template. Must be in the future.""" + + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] | str + """The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`).""" + + instruction_id: str + """Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.""" diff --git a/src/didit/types/sessions/batch_delete_response.py b/src/didit/types/sessions/batch_delete_response.py new file mode 100644 index 0000000..54648bc --- /dev/null +++ b/src/didit/types/sessions/batch_delete_response.py @@ -0,0 +1,97 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "SessionsBatchDeleteResponse", + "SessionsBatchDeleteResponse202", + "SessionsBatchDeleteResponse202Outcomes", + "SessionsBatchDeleteResponseResults", +] + + +class SessionsBatchDeleteResponseResults(BaseModel): + session_id: str | None = None + """The deleted session.""" + + session_number: int | None = None + """The deleted session's number.""" + + face_retention_outcome: ( + Literal[ + "retained_with_user", "deleted", "none", "ineligible_no_vendor_user", "failed_retryable" + ] + | str + | None + ) = None + """What happened to the session's face biometric data. `deleted`: the face embedding was deleted with the session (default). `retained_with_user`: the session is deleted and one image-free biometric template stays anchored to the User; see `biometric_template_uuid`. `none`: the session had no face embedding (for example a document-only KYC session without a liveness selfie; KYB sessions are never selected by this endpoint). `ineligible_no_vendor_user`: retention was requested but the session has no linked User, so the embedding was deleted with the session. `failed_retryable` (batch endpoint only): the group this session was deleted with could not be deleted (a storage service, a biometric store, or a monitoring provider was unavailable); this session is still in place, repeat the request for it.""" + + biometric_template_uuid: str | None = None + """Id of the retained biometric template when `face_retention_outcome` is `retained_with_user`; `null` otherwise. Manage it through `/v3/biometric-templates/{template_uuid}/`.""" + + biometric_template_expires_at: _dt.datetime | None = None + """When the retained biometric template is deleted, when `face_retention_outcome` is `retained_with_user`; `null` when no template is retained.""" + + deleted_child_verifications: int | None = None + """Always `0` on this endpoint, which deletes KYC sessions only. Deleting a Business Verification (KYB) session with `DELETE /v3/session/{sessionId}/delete/` reports the verifications it created that were deleted with it.""" + + +class SessionsBatchDeleteResponse(BaseModel): + results: list[SessionsBatchDeleteResponseResults] | None = None + + +class SessionsBatchDeleteResponse202Outcomes(BaseModel): + """How many sessions ended in each outcome. Every key is always present.""" + + deleted: int | None = None + """Sessions deleted together with their face embedding.""" + + none: int | None = None + """Sessions deleted that had no face embedding.""" + + retained_with_user: int | None = None + """Sessions deleted while one image-free biometric template was retained for their User, as the request or the application policy asked.""" + + ineligible_no_vendor_user: int | None = None + """Sessions deleted, with their face embedding, because retention was requested but they had no linked User.""" + + failed_retryable: int | None = None + """Sessions reported as not deleted. A job retries a group it could not delete by itself instead of counting it here.""" + + already_deleted: int | None = None + """Selected sessions that were already deleted, for example by another request, when the job reached them.""" + + +class SessionsBatchDeleteResponse202(BaseModel): + """A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.""" + + job_id: str | None = None + """Poll `GET /v3/sessions/delete/{job_id}/` with this id.""" + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + """`PENDING`: accepted, not started yet. `PROCESSING`: deleting; `processed` and `outcomes` are updating. `COMPLETED`: every selected session is accounted for. `FAILED`: the job stopped early; see `error`.""" + + total: int | None = None + """Sessions selected when the deletion was requested.""" + + processed: int | None = None + """Sessions the job has finished with so far.""" + + outcomes: SessionsBatchDeleteResponse202Outcomes | None = None + """How many sessions ended in each outcome. Every key is always present.""" + + deleted_child_verifications: int | None = None + """Verifications that the deleted business verifications created, deleted with them. Always `0` for a job started by `POST /v3/sessions/delete/`, which deletes KYC sessions only.""" + + error: str | None = None + """`null` unless `status` is `FAILED`; then why the deletion stopped. The sessions the job had not reached are still in place: repeat the request to delete them.""" + + created_at: _dt.datetime | None = None + """When the deletion was requested.""" + + completed_at: _dt.datetime | None = None + """When the job finished; `null` while it runs.""" diff --git a/src/didit/types/sessions/bulk_update_aml_hit_status_params.py b/src/didit/types/sessions/bulk_update_aml_hit_status_params.py new file mode 100644 index 0000000..84eb276 --- /dev/null +++ b/src/didit/types/sessions/bulk_update_aml_hit_status_params.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsBulkUpdateAmlHitStatusParams", "SessionsBulkUpdateAmlHitStatusParamsHitUpdates"] + + +class SessionsBulkUpdateAmlHitStatusParamsHitUpdates(TypedDict, total=False): + hit_id: Required[str] + + review_status: Required[ + Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] | str + ] + + +class SessionsBulkUpdateAmlHitStatusParams(TypedDict, total=False): + hit_updates: Required[Sequence[SessionsBulkUpdateAmlHitStatusParamsHitUpdates]] + """Non-empty list of hit updates.""" + + node_id: str + """Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks.""" diff --git a/src/didit/types/sessions/create_params.py b/src/didit/types/sessions/create_params.py new file mode 100644 index 0000000..f2f3d97 --- /dev/null +++ b/src/didit/types/sessions/create_params.py @@ -0,0 +1,226 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = [ + "SessionsCreateParams", + "SessionsCreateParamsContactDetails", + "SessionsCreateParamsExpectedDetails", +] + + +class SessionsCreateParamsContactDetails(TypedDict, total=False): + """User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number.""" + + email: str + """Email address of the user (e.g., "john.doe@example.com") that will be used during the [Email Verification](/core-technology/email-verification/overview) step. If not provided, the user must provide it during the verification flow.""" + + send_notification_emails: bool + """If true and an email is provided, Didit sends the initial "Verify your identity" email asynchronously when the User Verification (KYC) or Business Verification (KYB) session is created. Didit also sends verification status notifications for sessions requiring manual review to the provided email address (e.g., from 'In Review' to 'Approved' or 'Declined'). This helps users return to your application once their verification is complete. If you have white-label activated for the session, the email sent will be white-labeled.""" + + email_lang: ( + Literal[ + "en", + "ar", + "bg", + "bn", + "bs", + "ca", + "cnr", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh", + ] + | str + ) + """Language code (ISO 639-1) for email notifications. Controls the language of all email communications (e.g., "en", "es", "fr"). There is no stored default - when omitted, the verification invite email simply falls back to English (`en`) at send time. The enum below is a snapshot; the live source of truth for accepted values is the [supported languages](/integration/supported-languages) doc - always consult it before relying on a specific code.""" + + phone: str + """Phone number in E.164 format (e.g., "+14155552671") that will be used during the [Phone Verification](/core-technology/phone-verification/overview) step. If not provided, the user must provide it during the verification flow. Ignored for Business Verification (KYB) workflows. **Important:** This phone number is only enforced if it is a valid E.164 phone number. If the provided number is invalid or cannot be parsed, it will be ignored and the user will be able to input any valid phone number during the Phone Verification step.""" + + +class SessionsCreateParamsExpectedDetails(TypedDict, total=False): + """Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored.""" + + first_name: str + """User's first name. For example, `John`. The matching uses fuzzy comparison, and you can tune the strictness by configuring the name match score threshold in the Console for both ID Document and Proof of Address workflows.""" + + last_name: str + """User's last name. For example, `Doe`. The matching uses fuzzy comparison, and you can tune the strictness by configuring the name match score threshold in the Console for both ID Document and Proof of Address workflows.""" + + date_of_birth: str | _dt.date + """User's date of birth with format: YYYY-MM-DD. For example, `1990-05-15`.""" + + gender: Literal["M", "F"] | str | None + """User's gender. Must be either 'M', 'F', or null.""" + + nationality: str + """ISO 3166-1 alpha-3 country code representing the applicant's country of origin. For example, `USA`. See the [full list of supported country codes](https://docs.didit.me/core-technology/id-verification/supported-documents-id-verification#supported-documents-by-country).""" + + country: str + """ISO 3166-1 alpha-3 country code used as a fallback for both ID Verification and Proof of Address country-mismatch checks. Required when `address` is provided and neither `id_country` nor `poa_country` is set.""" + + id_country: str + """ISO 3166-1 alpha-3 country code representing the expected country of the applicant's ID document, which may differ from nationality. For example, `GBR`. Takes priority over `country` for ID verification country mismatch checks. See the [full list of supported country codes](https://docs.didit.me/core-technology/id-verification/supported-documents-id-verification#supported-documents-by-country).""" + + poa_country: str + """ISO 3166-1 alpha-3 country code representing the expected country of the Proof of Address document. For example, `USA`. Takes priority over `country` for POA country mismatch checks. See the [full list of supported country codes](https://docs.didit.me/core-technology/id-verification/supported-documents-id-verification#supported-documents-by-country).""" + + address: str + """The address in a human readable format, including as much information as possible. For example, `123 Main St, San Francisco, CA 94105, USA`. When provided, you must also pass `country` or `poa_country`, otherwise the request fails with `400`.""" + + identification_number: str + """The user's document number, personal number, or tax number. For example, `123456789`.""" + + ip_address: str + """Expected IP address for the session, in IPv4 or IPv6 format. If the actual IP address differs from this value, a warning will be logged. For example, `192.168.1.100` or `2001:db8::1`.""" + + expected_document_types: Sequence[Literal["P", "ID", "DL", "RP", "HIC", "TC", "SSC"] | str] + """Restricts the document types the user can present during the ID verification (OCR) step. When set, the document selection screen only shows the requested types and the returned `documents_allowed` is filtered accordingly. Values are case-insensitive and deduplicated; unknown values are rejected with `400`. Allowed codes: `P` (passport), `ID` (national ID), `DL` (driver's license), `RP` (residence permit), `HIC` (health insurance card), `TC` (tax card), `SSC` (social security card).""" + + company_name: str + """Expected legal name of the business being verified. For example, `Tesco PLC`. Pre-fills the company-name field of the registry search in Business Verification (KYB) workflows; when `registration_number` is also provided, the registry lookup combines both identifiers. Ignored for KYC workflows.""" + + registry_country: str + """ISO 3166-1 alpha-2 code of the country whose company registry the business is registered in. For example, `GB`. Where company registries operate at state or province level, append the ISO 3166-2 subdivision code as `XX-YY`: `US-CA` targets the California registry. Same format as `country_code` on the [KYB Registry Search API](https://docs.didit.me/standalone-apis/kyb-registry-search). Pre-selects the registry country in Business Verification (KYB) workflows; ignored for KYC workflows.""" + + registration_number: str + """Expected registration number of the business in the company registry. For example, `00445790`. Pre-fills the registration-number field of the registry search in Business Verification (KYB) workflows; when `company_name` is also provided, the registry lookup combines both identifiers. Ignored for KYC workflows.""" + + +class SessionsCreateParams(TypedDict, total=False): + workflow_id: Required[str] + """Stable identifier of the workflow that defines which verification steps the session will run. Workflows are created and managed in the [Workflows](/console/workflows) page of the Console. The `workflow_id` also implicitly selects whether the session is KYC or KYB.""" + + vendor_data: str + """A unique identifier for the user being verified, such as a UUID, email, or internal user ID. This field is used for: (1) **User grouping** - sessions with the same vendor_data are linked to the same user profile in the Users tab. (2) **Cross-session duplicate detection** - when checking for duplicated faces, documents, phone numbers, emails, IP addresses, or device fingerprints, sessions with the same vendor_data are treated as the same user and excluded from matches. Without vendor_data, every session is treated as a unique user and all potential duplicates are surfaced. We strongly recommend always providing a vendor_data to reduce noise in duplicate detection.""" + + callback: str + """URL to redirect the user to after verification completes. Didit automatically appends `verificationSessionId` and `status` (Approved, Declined, In Review) as query parameters. Custom URL schemes (e.g. `myapp://`) are supported for mobile callbacks. If omitted, the workflow's default `callback_url` is used.""" + + callback_method: Literal["initiator", "completer", "both"] | str + """Determines which device should handle the redirect to the provided callback URL. Use `initiator` to redirect only the device that started the flow, `completer` for the device that finishes it, or `both` to allow either device to trigger the callback. If you ever notice the callback not triggering reliably, we recommend setting this value to `both`.""" + + metadata: object + """Arbitrary JSON stored with the session and echoed back in the response and webhooks. Any JSON value is accepted (object, string, number, array), but a JSON object of key/value pairs is recommended. Not shown to the end user. Use it to pass your own correlation ids, A/B variants, or business context.""" + + language: ( + Literal[ + "en", + "ar", + "bg", + "bn", + "bs", + "ca", + "cnr", + "cs", + "da", + "de", + "el", + "es", + "et", + "fa", + "fi", + "fr", + "he", + "hi", + "hr", + "hu", + "hy", + "id", + "it", + "ja", + "ka", + "kk", + "ko", + "ky", + "lt", + "lv", + "mk", + "mn", + "ms", + "nl", + "no", + "pl", + "pt-BR", + "pt", + "ro", + "ru", + "sk", + "sl", + "so", + "sq", + "sr", + "sv", + "th", + "tr", + "uk", + "uz", + "vi", + "zh-CN", + "zh-TW", + "zh", + ] + | str + ) + """Language code (ISO 639-1) for the verification process interface. Controls the language displayed to the end user during verification. If not provided, the browser's language will be automatically detected and used. Check all the supported languages [here](/integration/supported-languages).""" + + contact_details: SessionsCreateParamsContactDetails + """User contact information that can be used for notifications, prefilling verification forms, and phone verification. This includes email address, preferred language for communications, and phone number.""" + + expected_details: SessionsCreateParamsExpectedDetails + """Expected user details used to cross-validate against the data extracted from the user's ID document, Proof of Address, and other verification steps. Mismatches are surfaced as warnings on the decision; some fields (e.g. `id_country`, `expected_document_types`) also alter the user-facing flow. For Business Verification (KYB) workflows only the business fields (`company_name`, `registry_country`, `registration_number`) are used - they pre-fill the company registry search for the user - and all person-level fields are ignored.""" + + portrait_image: str + """Base64-encoded portrait image of the end user's face (max 2MB; JPEG, PNG, WebP, or TIFF). Used as the reference image to match against the liveness capture when the workflow is a `Biometric Authentication` workflow with Face Match enabled, or any graph workflow where Face Match runs before ID Verification (OCR). Optional when the `vendor_data` user already has a stored face image: Didit reuses it automatically, resolved in priority order: approved liveness face, ePassport chip photo, ID document portrait, manually enrolled profile face (only faces from approved sessions or faces you enrolled yourself are reused). When provided, `portrait_image` always takes precedence over the stored face. If omitted and no stored face exists (or `vendor_data` is not sent), session creation fails with `400`. Ignored for other workflow types.""" + + sandbox_scenario: str + """Sandbox-only scenario slug. Only accepted when the API key belongs to a sandbox-mode application; rejected on live applications. When set, the create flow expands the scenario's bundled magic values onto the session's input fields (contact_details, expected_details) so the mocked providers reproduce the scenario's outcome through the real workflow pipeline. See the scenario catalog at GET /v1/sandbox/scenarios/ or [Sandbox & Test Data](/integration/sandbox-testing). You can also arm (or re-arm) a scenario after create via POST /v3/session/{sessionId}/sandbox/arm/, as long as the session is still `Not Started`. For non-doc lookup, use `lookup_partial_match`, `lookup_no_match`, or `lookup_provider_error` to test the configured attempt budget and fallback. For identity wallets, use `wallet_cancelled`, `wallet_timeout`, or `wallet_provider_error`; `approve` simulates a verified wallet. All catalog wallets can be tested in sandbox, including schemes unavailable on live applications. Simulated attempts are not charged.""" diff --git a/src/didit/types/sessions/create_response.py b/src/didit/types/sessions/create_response.py new file mode 100644 index 0000000..e2d24dd --- /dev/null +++ b/src/didit/types/sessions/create_response.py @@ -0,0 +1,54 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsCreateResponse"] + + +class SessionsCreateResponse(BaseModel): + session_id: str + """Unique identifier of the verification session. Use this id when calling `GET /v3/session/{sessionId}/decision/`.""" + + session_number: int + """Sequential, human-friendly number assigned to the session inside your application. Useful for support and dashboards.""" + + session_token: str + """12-character URL-safe token that authorizes the end user to access the hosted verification flow at `url`. Valid until the session expires. Treat it as a secret - anyone holding it can open the verification flow for this session.""" + + url: str + """Hosted verification URL to redirect the end user to. The URL embeds the `session_token` and, if configured, uses your white-label domain. When the request includes `language`, the URL contains a language path segment - `https://verify.didit.me/{language}/session/{session_token}` (e.g. `/es/session/...`); without `language`, the segment is omitted.""" + + vendor_data: str | None + """Identifier you passed in the request to link the session to a user or business in your own system. Echoed back verbatim. Null when not provided.""" + + metadata: object + """Arbitrary JSON payload you stored with the session at creation time. Echoed back verbatim - whatever JSON value you sent (object, string, number, array) is returned as-is, though a JSON object is recommended. Not shown to the end user. Always present in responses; `null` when not provided at creation time.""" + + status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + ) + """Current status of the session. Newly created sessions return `Not Started`. If a non-finished session already existed for the same `vendor_data` on the workflow's latest published version, the returned status reflects that existing session.""" + + callback: str | None + """Final redirect URL the user is sent to after completing the flow. Didit appends `?verificationSessionId={session_id}&status={status}` to this URL. Falls back to the workflow's configured callback URL when not provided in the request; `null` when neither is set.""" + + workflow_id: str + """Stable identifier of the workflow this session runs on. Always the workflow's stable group identifier - even if you referenced a specific workflow version UUID in the request (backward-compatible lookup), the response carries the stable `workflow_id`. Use it to correlate the session with the workflow you configured in the Console.""" + + workflow_version: int + """Version number of the published workflow version the session was pinned to at creation. Didit resolves `workflow_id` to the workflow's latest published version when the session is created, and the session keeps running that version even if the workflow is republished later.""" diff --git a/src/didit/types/sessions/delete_params.py b/src/didit/types/sessions/delete_params.py new file mode 100644 index 0000000..df184cc --- /dev/null +++ b/src/didit/types/sessions/delete_params.py @@ -0,0 +1,26 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["SessionsDeleteParams"] + + +class SessionsDeleteParams(TypedDict, total=False): + retain_face_embeddings: bool | None + """Override the application's `face_retention_policy` for this deletion. `true` keeps one image-free face biometric template anchored to the session's User after the session is deleted; `false` deletes the face embedding with the session; omit or `null` to follow the application policy. Ignored for KYB sessions and for sessions without a face embedding.""" + + face_retention_days: int + """Finite retention duration, in days, for a retained template. Required when a template is retained unless the application already sets `face_retention_days`. The template's `expires_at` is the earliest of this duration, the application's general data-retention window, and `face_retention_deadline`.""" + + face_retention_deadline: str | _dt.datetime + """Optional hard expiry for a retained template. Must be in the future.""" + + deletion_instruction: Literal["operational_session_delete", "privacy_erasure"] | str + """The class of instruction you are giving. `operational_session_delete` deletes the session and retains a template only when the policy or `retain_face_embeddings` says so. `privacy_erasure` purges every retained biometric template anchored to the session's User before deleting the session, cannot be overridden by the application policy, and cannot be combined with `retain_face_embeddings: true` (`400`).""" + + instruction_id: str + """Your durable reference for this deletion instruction (for example an erasure-ticket id). Recorded on the audit trail and on any retained template. Generated by Didit when omitted.""" diff --git a/src/didit/types/sessions/delete_response.py b/src/didit/types/sessions/delete_response.py new file mode 100644 index 0000000..82afb7d --- /dev/null +++ b/src/didit/types/sessions/delete_response.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsDeleteResponse"] + + +class SessionsDeleteResponse(BaseModel): + session_id: str | None = None + """The deleted session.""" + + session_number: int | None = None + """The deleted session's number.""" + + face_retention_outcome: ( + Literal["retained_with_user", "deleted", "none", "ineligible_no_vendor_user"] | str | None + ) = None + """What happened to the session's face biometric data. `deleted`: the face embedding was deleted with the session (default). `retained_with_user`: the session is deleted and one image-free biometric template stays anchored to the User; see `biometric_template_uuid`. `none`: the session had no face embedding (for example a KYB session, or a KYC session without a liveness selfie). `ineligible_no_vendor_user`: retention was requested but the session has no linked User, so the embedding was deleted with the session. A biometric-store failure is never reported through this field on this endpoint: it returns `503` and the session is not deleted.""" + + biometric_template_uuid: str | None = None + """Id of the retained biometric template when `face_retention_outcome` is `retained_with_user`; `null` otherwise. Manage it through `/v3/biometric-templates/{template_uuid}/`.""" + + biometric_template_expires_at: _dt.datetime | None = None + """When the retained biometric template is deleted, when `face_retention_outcome` is `retained_with_user`; `null` when no template is retained.""" + + deleted_child_verifications: int | None = None + """For a Business Verification (KYB) session: how many verifications it created (its key people's KYC sessions and its corporate owners' KYB sessions, recursively) were deleted with it. Always `0` for a KYC session.""" diff --git a/src/didit/types/sessions/import_shared_params.py b/src/didit/types/sessions/import_shared_params.py new file mode 100644 index 0000000..ad3f3ef --- /dev/null +++ b/src/didit/types/sessions/import_shared_params.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsImportSharedParams"] + + +class SessionsImportSharedParams(TypedDict, total=False): + share_token: Required[str] + """JWT share token issued by [`POST /v3/session/{sessionId}/share/`](/sessions-api/share-session).""" + + trust_review: Required[bool] + """If `true`, the cloned session keeps the source's final `status`. If `false`, it is forced into `In Review`.""" + + workflow_id: Required[str] + """UUID of a workflow in the calling application. Cross-application IDs are rejected with `404`.""" + + vendor_data: str | None + """Optional override for the cloned session's `vendor_data`.""" diff --git a/src/didit/types/sessions/imports/__init__.py b/src/didit/types/sessions/imports/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/sessions/imports/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/sessions/imports/create_params.py b/src/didit/types/sessions/imports/create_params.py new file mode 100644 index 0000000..16a729f --- /dev/null +++ b/src/didit/types/sessions/imports/create_params.py @@ -0,0 +1,29 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +from didit._types import FileTypes + +__all__ = ["SessionsImportsCreateParams"] + + +class SessionsImportsCreateParams(TypedDict, total=False): + import_type: ( + Literal["user_verification", "business_verification", "status_rules", "transactions"] | str + ) + """What to import: historical User Verification sessions, historical Business Verification sessions, workflow custom status rules, or historical transaction monitoring records.""" + + workflow_id: str + """Workflow UUID that imported sessions or status rules should be attached to. Required for user_verification, business_verification, and status_rules. Omit for transactions.""" + + source_format: Literal["csv", "ndjson"] | str + """Optional. Inferred from the URL's file extension when omitted.""" + + source_file_url: str + """Alternative to file upload. Must be an HTTPS URL resolving to a public address (private/loopback networks are rejected). Mutually exclusive with `file`.""" + + file: FileTypes + """Canonical CSV or NDJSON file. Use NDJSON for large imports. Maximum upload size 25 MB (use `source_file_url` for bigger files). Mutually exclusive with `source_file_url`.""" diff --git a/src/didit/types/sessions/imports/list_errors_response.py b/src/didit/types/sessions/imports/list_errors_response.py new file mode 100644 index 0000000..5b7e46e --- /dev/null +++ b/src/didit/types/sessions/imports/list_errors_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.session_import_error import SessionImportError + +__all__ = ["SessionsImportsListErrorsResponse"] + + +class SessionsImportsListErrorsResponse(BaseModel): + count: int | None = None + """Number of error rows - exact up to 100, capped at 100 beyond that. Rely on `next` being `null` to detect the last page.""" + + next: str | None = None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[SessionImportError] | None = None diff --git a/src/didit/types/sessions/list_networks_response.py b/src/didit/types/sessions/list_networks_response.py new file mode 100644 index 0000000..fe4de94 --- /dev/null +++ b/src/didit/types/sessions/list_networks_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.entity_network_membership import EntityNetworkMembership + +__all__ = ["SessionsListNetworksResponse"] + + +class SessionsListNetworksResponse(BaseModel): + results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/sessions/list_response.py b/src/didit/types/sessions/list_response.py new file mode 100644 index 0000000..59cb35b --- /dev/null +++ b/src/didit/types/sessions/list_response.py @@ -0,0 +1,362 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "SessionsListResponse", + "SessionsListResponseResultsKybSessionRowSessionKindBusiness", + "SessionsListResponseResultsKycSessionRowSessionKindUser", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification", + "SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa", + "SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress", + "SessionsListResponseResultsKycSessionRowSessionKindUserFeatures", + "SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber", +] + + +class SessionsListResponseResultsKybSessionRowSessionKindBusiness(BaseModel): + """Summary row for a Business Verification (KYB) session. Returned only when `session_kind` is `business` or `all`.""" + + session_id: str + """Unique identifier of the business verification session.""" + + session_kind: Literal["business"] | str + """Row discriminator. Always `"business"` for KYB rows.""" + + session_number: int + """Human-friendly sequential number of the business session within your application. Numbered independently from KYC sessions.""" + + status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + ) + """Overall session status. See the [Verification Statuses](/integration/verification-statuses) page.""" + + vendor_data: str | None + """Your own identifier for the business, echoed back exactly as sent at session creation.""" + + created_at: _dt.datetime + """UTC timestamp when the session was created.""" + + workflow_id: str | None + """Identifier of the workflow this session was created with.""" + + workflow_type: str | None + """Workflow type. `kyb` for business workflows.""" + + workflow_label: str | None + """Display name of the workflow.""" + + company_name: str | None + """Name of the company being verified, from the registry company linked to the session. `null` until a company is selected.""" + + registration_number: str | None + """Company registration number from the registry company linked to the session.""" + + country: str | None + """Country code of the registry company linked to the session.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData( + BaseModel +): + """Subset of the data that was screened. Only non-empty keys are included.""" + + nationality: str | None = None + + document_number: str | None = None + + date_of_birth: str | None = None + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml(BaseModel): + """Present when AML screening ran.""" + + total_hits: int | None = None + """Number of AML screening hits.""" + + entity_type: str | None = None + """Screened entity type.""" + + is_adverse_media_fetched: bool | None = None + + screened_data: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAmlScreenedData + | None + ) = None + """Subset of the data that was screened. Only non-empty keys are included.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation( + BaseModel +): + """Present when database validation ran.""" + + validation_type: str | None = None + + match_type: str | None = None + + identification_number: str | None = None + """Tax, personal, or document number that was validated (first available).""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification( + BaseModel +): + """Present when email verification ran.""" + + is_breached: bool | None = None + + is_disposable: bool | None = None + + is_undeliverable: bool | None = None + + is_blocklisted: bool | None = None + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch(BaseModel): + """Present when face match ran.""" + + score: float | None = None + """Face-match similarity score, rounded to 2 decimals.""" + + source_image: str | None = None + """Expiring (presigned) URL of the source face image.""" + + target_image: str | None = None + """Expiring (presigned) URL of the target face image.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness(BaseModel): + """Present when a liveness check ran.""" + + method: str | None = None + + score: float | None = None + """Liveness confidence score, rounded to 2 decimals.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification( + BaseModel +): + """Present when phone verification ran.""" + + country_code: str | None = None + """ISO 3166-1 alpha-2 country of the phone number.""" + + is_temporary: bool | None = None + + is_ported: bool | None = None + + is_blocked: bool | None = None + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa(BaseModel): + """Present when proof of address ran.""" + + document_type: str | None = None + + issuer: str | None = None + + address: str | None = None + + name_on_document: str | None = None + + +class SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails(BaseModel): + """Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.""" + + aml: SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsAml | None = None + """Present when AML screening ran.""" + + database_validation: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsDatabaseValidation + | None + ) = None + """Present when database validation ran.""" + + poa: SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPoa | None = None + """Present when proof of address ran.""" + + liveness: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsLiveness | None + ) = None + """Present when a liveness check ran.""" + + face_match: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsFaceMatch | None + ) = None + """Present when face match ran.""" + + phone_verification: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsPhoneVerification + | None + ) = None + """Present when phone verification ran.""" + + email_verification: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetailsEmailVerification + | None + ) = None + """Present when email verification ran.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress(BaseModel): + """Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.""" + + email: str + + is_verified: bool + """`true` only when the email-verification feature approved this address.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserFeatures(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACE_MATCH", + "POA", + "QUESTIONNAIRE", + "EMAIL_VERIFICATION", + "PHONE", + "AML", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "DATABASE_VALIDATION", + ] + | str + ) + """Verification feature identifier.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str + """Status of this feature. `Not Finished` until the step completes.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber(BaseModel): + """Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.""" + + number: str + """Phone number in E.164 format.""" + + is_verified: bool + """`true` only when the phone-verification feature approved this number.""" + + +class SessionsListResponseResultsKycSessionRowSessionKindUser(BaseModel): + """Summary row for a User Verification (KYC) session. Identity fields (`full_name`, `country`, `document_type`, `portrait_image`, contact fields) are populated progressively as the user completes verification steps, and are `null` until the relevant step finishes. Fields restricted by the workflow's response-attributes configuration are also returned as `null`.""" + + session_id: str + """Unique identifier of the verification session. Use it with `GET /v3/session/{sessionId}/decision/` to fetch the full decision payload.""" + + session_kind: Literal["user"] | str + """Row discriminator. Always `"user"` for KYC rows; Business Verification (KYB) rows have `"business"` and a different shape.""" + + session_number: int + """Human-friendly sequential number of the session within your application. Also matched by the `search` parameter.""" + + session_url: str | None + """Hosted verification URL the end user opens to complete the flow. `null` when the session has no hosted URL.""" + + portrait_image: str | None + """Expiring (presigned) URL of the best available portrait: the NFC chip photo when available, otherwise the ID document portrait, otherwise the liveness reference image. `null` until one of those exists. Download promptly; the link expires.""" + + document_type: ( + Literal[ + "Passport", + "Identity Card", + "Driver's License", + "Residence Permit", + "Health Insurance Card", + "Tax Card", + ] + | str + | None + ) + """Human-readable type of the verified ID document. `null` before document verification completes, and also `null` after completion when the verified code has no display mapping (`SSC`, `WP`, `VISA`, `PSC`, `BC`, `OTHER`). Note: the `document_type` query parameter filters by short codes (`P`, `ID`, `DL`, `RP`, ...), not by these display values.""" + + full_name: str | None + """Best available full name for the session, resolved in order: ID document OCR, AML screened data, database-validation screened data, proof-of-address document, then the expected details provided at session creation. `null` when none is available yet.""" + + country: str | None + """ISO 3166-1 alpha-3 country associated with the session, resolved in order: ID document issuing state, database-validation issuing state, proof-of-address issuing state, expected details, then the verified phone's country. May differ from nationality.""" + + status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + ) + """Overall session status. See the [Verification Statuses](/integration/verification-statuses) page for the lifecycle.""" + + vendor_data: str | None + """Your own identifier for the end user, echoed back exactly as sent at session creation.""" + + didit_internal_id: str | None + """Didit's internal identifier for the verified user profile behind this session. Stable across multiple sessions of the same user; filterable via the `didit_internal_id` query parameter.""" + + created_at: _dt.datetime + """UTC timestamp when the session was created.""" + + features: list[SessionsListResponseResultsKycSessionRowSessionKindUserFeatures] + """Per-feature status for every verification step configured in the session's workflow, in workflow order.""" + + phone_number: SessionsListResponseResultsKycSessionRowSessionKindUserPhoneNumber | None + """Phone associated with the session: the verified phone when phone verification ran, otherwise the contact phone provided at session creation. `null` when neither exists.""" + + email_address: SessionsListResponseResultsKycSessionRowSessionKindUserEmailAddress | None + """Email associated with the session: the verified email when email verification ran, otherwise the contact email provided at session creation. `null` when neither exists.""" + + additional_details: ( + SessionsListResponseResultsKycSessionRowSessionKindUserAdditionalDetails | None + ) + """Compact per-feature extras. Only features that produced data appear as keys; `null` when no feature produced extras. Fields excluded by the workflow's response-attributes configuration are omitted.""" + + +class SessionsListResponse(BaseModel): + count: int + """Number of sessions matching the filters. Exact for KYC rows; for KYB rows it is capped at 100 (use `next` to know whether more pages exist). For `session_kind=all` it is the sum of both kinds.""" + + next: str | None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[ + SessionsListResponseResultsKycSessionRowSessionKindUser + | SessionsListResponseResultsKybSessionRowSessionKindBusiness + ] + """Session rows, newest first. With `session_kind=all`, KYC rows come first, then KYB rows.""" diff --git a/src/didit/types/sessions/preview_resubmission_params.py b/src/didit/types/sessions/preview_resubmission_params.py new file mode 100644 index 0000000..2e1b957 --- /dev/null +++ b/src/didit/types/sessions/preview_resubmission_params.py @@ -0,0 +1,55 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsPreviewResubmissionParams", "SessionsPreviewResubmissionParamsNodesToResubmit"] + + +class SessionsPreviewResubmissionParamsNodesToResubmit(TypedDict, total=False): + node_id: Required[str] + """Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).""" + + feature: Required[ + Literal[ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_DOCUMENTS", + "ID_VERIFICATION", + "POA", + "PHONE", + "EMAIL", + "KYB_REGISTRY", + "KYB_KEY_PEOPLE", + "KYB", + ] + | str + ] + """Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.""" + + +class SessionsPreviewResubmissionParams(TypedDict, total=False): + nodes_to_resubmit: Sequence[SessionsPreviewResubmissionParamsNodesToResubmit] + """Steps to preview, using the same validation, selection and workflow ordering as a resubmission. Omit to auto-select recorded attempts needing resubmission.""" + + email_address: Required[str] + """Recipient shown in the preview. No email is sent.""" + + email_language: str + """Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).""" diff --git a/src/didit/types/sessions/preview_resubmission_response.py b/src/didit/types/sessions/preview_resubmission_response.py new file mode 100644 index 0000000..2ac709f --- /dev/null +++ b/src/didit/types/sessions/preview_resubmission_response.py @@ -0,0 +1,66 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "SessionsPreviewResubmissionResponse", + "SessionsPreviewResubmissionResponseNodesToResubmit", +] + + +class SessionsPreviewResubmissionResponseNodesToResubmit(BaseModel): + node_id: str + """Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).""" + + feature: ( + Literal[ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_DOCUMENTS", + "ID_VERIFICATION", + "POA", + "PHONE", + "EMAIL", + "KYB_REGISTRY", + "KYB_KEY_PEOPLE", + "KYB", + ] + | str + ) + """Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.""" + + +class SessionsPreviewResubmissionResponse(BaseModel): + recipient: str + + sender: str + + subject: str + + html: str + """Rendered email. Inline image attachments use data URLs for preview.""" + + nodes_to_resubmit: list[SessionsPreviewResubmissionResponseNodesToResubmit] + + requires_user_action: bool + """Whether at least one selected check requires user interaction. This does not predict the final workflow outcome.""" + + preview_token: str + """Signed confirmation receipt, valid for 15 minutes. Pass it as email_preview_token when confirming the same message.""" diff --git a/src/didit/types/sessions/retrieve_decision_response.py b/src/didit/types/sessions/retrieve_decision_response.py new file mode 100644 index 0000000..86de39e --- /dev/null +++ b/src/didit/types/sessions/retrieve_decision_response.py @@ -0,0 +1,4140 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.document_ai_detected_code import DocumentAIDetectedCode +from didit.types.components.kyb_registry_address import KYBRegistryAddress +from didit.types.components.kyb_registry_industry import KYBRegistryIndustry +from didit.types.components.kyb_registry_ownership import KYBRegistryOwnership +from didit.types.components.kyb_registry_shareholding import KYBRegistryShareholding + +__all__ = [ + "SessionsRetrieveDecisionResponse", + "SessionsRetrieveDecisionResponseAmlScreenings", + "SessionsRetrieveDecisionResponseAmlScreeningsHits", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData", + "SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails", + "SessionsRetrieveDecisionResponseAmlScreeningsScreenedData", + "SessionsRetrieveDecisionResponseAmlScreeningsWarnings", + "SessionsRetrieveDecisionResponseContactDetails", + "SessionsRetrieveDecisionResponseDatabaseValidations", + "SessionsRetrieveDecisionResponseDatabaseValidationsErrors", + "SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData", + "SessionsRetrieveDecisionResponseDatabaseValidationsValidations", + "SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation", + "SessionsRetrieveDecisionResponseDatabaseValidationsWarnings", + "SessionsRetrieveDecisionResponseDocumentAiDocuments", + "SessionsRetrieveDecisionResponseDocumentAiDocumentsItems", + "SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields", + "SessionsRetrieveDecisionResponseDocumentVerifications", + "SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue", + "SessionsRetrieveDecisionResponseDocumentVerificationsItems", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation", + "SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionsRetrieveDecisionResponseEmailVerifications", + "SessionsRetrieveDecisionResponseEmailVerificationsBreaches", + "SessionsRetrieveDecisionResponseEmailVerificationsLifecycle", + "SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails", + "SessionsRetrieveDecisionResponseEmailVerificationsMatches", + "SessionsRetrieveDecisionResponseEmailVerificationsWarnings", + "SessionsRetrieveDecisionResponseExpectedDetails", + "SessionsRetrieveDecisionResponseFaceMatches", + "SessionsRetrieveDecisionResponseFaceMatchesWarnings", + "SessionsRetrieveDecisionResponseIdVerifications", + "SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches", + "SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails", + "SessionsRetrieveDecisionResponseIdVerificationsExtraFields", + "SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom", + "SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore", + "SessionsRetrieveDecisionResponseIdVerificationsIdLookup", + "SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison", + "SessionsRetrieveDecisionResponseIdVerificationsMatches", + "SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails", + "SessionsRetrieveDecisionResponseIdVerificationsMrz", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddress", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints", + "SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation", + "SessionsRetrieveDecisionResponseIdVerificationsWalletVerification", + "SessionsRetrieveDecisionResponseIdVerificationsWarnings", + "SessionsRetrieveDecisionResponseIpAnalyses", + "SessionsRetrieveDecisionResponseIpAnalysesIdDocument", + "SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation", + "SessionsRetrieveDecisionResponseIpAnalysesIp", + "SessionsRetrieveDecisionResponseIpAnalysesIpLocation", + "SessionsRetrieveDecisionResponseIpAnalysesMatches", + "SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo", + "SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo", + "SessionsRetrieveDecisionResponseIpAnalysesPoaDocument", + "SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation", + "SessionsRetrieveDecisionResponseIpAnalysesWarnings", + "SessionsRetrieveDecisionResponseKeyPeopleChecks", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories", + "SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles", + "SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures", + "SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary", + "SessionsRetrieveDecisionResponseLivenessChecks", + "SessionsRetrieveDecisionResponseLivenessChecksMatches", + "SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails", + "SessionsRetrieveDecisionResponseLivenessChecksWarnings", + "SessionsRetrieveDecisionResponseNfcVerifications", + "SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity", + "SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary", + "SessionsRetrieveDecisionResponseNfcVerificationsChipData", + "SessionsRetrieveDecisionResponseNfcVerificationsWarnings", + "SessionsRetrieveDecisionResponsePhoneVerifications", + "SessionsRetrieveDecisionResponsePhoneVerificationsCarrier", + "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle", + "SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails", + "SessionsRetrieveDecisionResponsePhoneVerificationsMatches", + "SessionsRetrieveDecisionResponsePhoneVerificationsWarnings", + "SessionsRetrieveDecisionResponsePoaVerifications", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation", + "SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions", + "SessionsRetrieveDecisionResponsePoaVerificationsExtraFields", + "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress", + "SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation", + "SessionsRetrieveDecisionResponsePoaVerificationsWarnings", + "SessionsRetrieveDecisionResponseQuestionnaireResponses", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSections", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer", + "SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices", + "SessionsRetrieveDecisionResponseRegistryChecks", + "SessionsRetrieveDecisionResponseRegistryChecksCompany", + "SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners", + "SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers", + "SessionsRetrieveDecisionResponseResubmissions", + "SessionsRetrieveDecisionResponseResubmissionsNodes", + "SessionsRetrieveDecisionResponseResubmissionsRequestedBy", + "SessionsRetrieveDecisionResponseReviews", +] + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails(BaseModel): + sentiment: str | None = None + + entity_type: str | None = None + + sentiment_score: int | None = None + + adverse_keywords: dict[str, int] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches(BaseModel): + country: str | None = None + + summary: str | None = None + + headline: str | None = None + + sentiment: str | None = None + + thumbnail: str | None = None + + source_url: str | None = None + + author_name: str | None = None + + other_sources: list[str] | None = None + + adverse_keywords: list[str] | None = None + + sentiment_score: int | None = None + + publication_date: _dt.datetime | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities(BaseModel): + name: list[str] | None = None + + active: list[str] | None = None + + status: list[str] | None = None + + details: list[str] | None = None + + relation: list[str] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches(BaseModel): + aliases: list[str] | None = None + + education: list[str] | None = None + + list_name: str | None = None + + publisher: str | None = None + + source_url: str | None = None + + description: str | None = None + + matched_name: str | None = None + + pep_position: str | None = None + + date_of_birth: str | None = None + + other_sources: list[str] | None = None + + place_of_birth: str | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties(BaseModel): + name: list[str] | None = None + + alias: list[str] | None = None + + notes: list[str] | None = None + + title: str | None = None + + gender: list[str] | None = None + + height: str | None = None + + topics: str | None = None + + weight: str | None = None + + address: str | None = None + + country: str | None = None + + website: str | None = None + + eyeColor: str | None = None + + keywords: str | None = None + + lastName: list[str] | None = None + + position: str | None = None + + religion: str | None = None + + birthDate: list[str] | None = None + + education: list[str] | None = None + + ethnicity: str | None = None + + firstName: list[str] | None = None + + hairColor: str | None = None + + weakAlias: str | None = None + + birthPlace: list[str] | None = None + + modifiedAt: str | None = None + + wikidataId: str | None = None + + citizenship: str | None = None + + nationality: str | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories(BaseModel): + score: int | None = None + + weightage: int | None = None + + risk_level: str | None = None + + risk_scores: dict[str, object] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries(BaseModel): + score: int | None = None + + weightage: int | None = None + + risk_level: str | None = None + + risk_scores: dict[str, object] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes(BaseModel): + score: int | None = None + + weightage: int | None = None + + risk_level: str | None = None + + risk_scores: dict[str, object] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView(BaseModel): + crimes: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCrimes | None = None + + countries: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCountries | None = None + + categories: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskViewCategories | None = None + + custom_list: dict[str, object] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches(BaseModel): + list_name: list[str] | None = None + """Name(s) of the sanction list(s)""" + + matched_name: str | None = None + """Name matched in the sanction list""" + + description: str | None = None + """Description of the sanction entry""" + + reason: str | None = None + """Free-text reason why the entity was sanctioned. Joined from the sanction list's reason entries. Examples: 'Executive Order 13224 (Terrorism)', 'Support for actions undermining Ukraine's sovereignty', 'Listed due to national security concerns.'""" + + source_url: str | None = None + """URL to the source of the sanction""" + + other_sources: list[str] | None = None + """Additional source URLs""" + + details: list[str] | None = None + """Additional details about the sanction""" + + legal_basis: list[str] | None = None + """Legal basis for the sanction""" + + listed_on: list[str] | None = None + """Date(s) when the entity was listed""" + + sanction_list: list[str] | None = None + """Sanction list identifier(s)""" + + sanction_program: list[str] | None = None + """Sanction program(s) the entity is listed under""" + + sanctioning_authority: list[str] | None = None + """Authority/authorities that imposed the sanction""" + + updated_on: list[str] | None = None + """Date(s) when the sanction entry was last updated""" + + additional_data: dict[str, object] | None = None + """Additional data from the sanction source not captured in the main fields""" + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown(BaseModel): + """Detailed breakdown of how the match_score was calculated.""" + + name_score: int | None = None + """Raw name similarity percentage (0-100).""" + + name_weight: int | None = None + """Original weight for name from config (0-100).""" + + name_weight_normalized: float | None = None + """Normalized weight for name after re-weighting for missing data (0-100).""" + + name_contribution: float | None = None + """Points contributed by name to the total score.""" + + dob_score: int | None = None + """DOB match score (-100 to 100).""" + + dob_weight: int | None = None + """Original weight for DOB from config (0-100).""" + + dob_weight_normalized: float | None = None + """Normalized weight for DOB after re-weighting for missing data (0-100).""" + + dob_contribution: float | None = None + """Points contributed by DOB to the total score.""" + + country_score: int | None = None + """Country match score (-100 to 100).""" + + country_weight: int | None = None + """Original weight for country from config (0-100).""" + + country_weight_normalized: float | None = None + """Normalized weight for country after re-weighting for missing data (0-100).""" + + country_contribution: float | None = None + """Points contributed by country to the total score.""" + + document_number_match_type: Literal["MATCH", "NEUTRAL", "HARD_MISMATCH"] | str | None = None + """Result of document number 'Golden Key' evaluation.""" + + document_number_effect: str | None = None + """Human-readable description of the document number effect on the score.""" + + total_score: int | None = None + """Final weighted match score (0-100).""" + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails( + BaseModel +): + case_type: ( + Literal[ + "Accusation", + "Arrest", + "Bribery", + "Criminal", + "Fraud", + "Illegal Fundraising", + "Non-conviction Terror", + "Proclaimed Offender", + "Regulatory Enforcement", + "Terrorism", + "Terrorism-related", + "Terrorism-related listing", + ] + | str + | None + ) = None + """Type of the warning case""" + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData(BaseModel): + case_details: ( + list[ + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataDataCaseDetails + ] + | None + ) = None + """Details of warning cases associated with this match""" + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData(BaseModel): + """Additional data from the warning source. May contain a `data` object with `case_details` array.""" + + data: ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalDataData | None + ) = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches(BaseModel): + list_name: str | None = None + """Name of the warning list publisher""" + + matched_name: str | None = None + """Name matched in the warning list""" + + description: str | None = None + """Description of the warning entry""" + + publisher: str | None = None + """Publisher or source of the warning""" + + source_url: str | None = None + """URL to the source of the warning""" + + other_sources: list[str] | None = None + """Additional source URLs""" + + countries: list[str] | None = None + """Countries associated with the warning""" + + created_at: str | None = None + """Date the warning entry was created""" + + updated_at: str | None = None + """Date the warning entry was last updated""" + + additional_data: ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatchesAdditionalData | None + ) = None + """Additional data from the warning source. May contain a `data` object with `case_details` array.""" + + +class SessionsRetrieveDecisionResponseAmlScreeningsHits(BaseModel): + id: str | None = None + """Unique identifier of the match""" + + url: str | None = None + """Source URL for the match""" + + match: bool | None = None + """Whether this is a confirmed match""" + + score: float | None = None + """Deprecated. Deprecated weighted match score in the 0-1 range (e.g. `0.73`). Use `match_score` (0-100) instead.""" + + target: bool | None = None + """Whether the matched entity is the direct target of the listing (as opposed to a related party). `null` when the source does not specify it.""" + + caption: str | None = None + """Caption or title of the match""" + + datasets: ( + list[ + Literal[ + "PEP", + "PEP Level 1", + "PEP Level 2", + "PEP Level 3", + "PEP Level 4", + "Sanctions", + "Adverse Media", + "Warnings and Regulatory Enforcement", + "Fitness and Probity", + "SIP", + "SIE", + "Insolvency", + ] + | str + ] + | None + ) = None + """List of dataset categories where the match was found. Indicates whether the entity appeared in PEP lists (with level), sanctions databases, adverse media, warnings/regulatory enforcement, or other compliance datasets.""" + + features: dict[str, object] | None = None + """Additional features of the match""" + + rca_name: str | None = None + """RCA name if applicable""" + + last_seen: _dt.datetime | None = None + """Date when match was last seen""" + + risk_view: SessionsRetrieveDecisionResponseAmlScreeningsHitsRiskView | None = None + + first_seen: _dt.datetime | None = None + """Date when match was first seen""" + + properties: SessionsRetrieveDecisionResponseAmlScreeningsHitsProperties | None = None + + match_score: float | None = None + """Weighted identity confidence score (0-100) calculated from name similarity, date of birth, and country matching. Used to determine if hit is a False Positive or Possible Match.""" + + risk_score: float | None = None + """Entity risk score (0-100). Used to determine final AML status (Approved/In Review/Declined) based on the highest risk_score among non-False-Positive hits.""" + + review_status: ( + Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] | str | None + ) = None + """Review status for this hit. Can be 'Unreviewed', 'Confirmed Match', 'False Positive', or 'Inconclusive'.""" + + score_breakdown: SessionsRetrieveDecisionResponseAmlScreeningsHitsScoreBreakdown | None = None + """Detailed breakdown of how the match_score was calculated.""" + + pep_matches: list[SessionsRetrieveDecisionResponseAmlScreeningsHitsPepMatches] | None = None + + linked_entities: ( + list[SessionsRetrieveDecisionResponseAmlScreeningsHitsLinkedEntities] | None + ) = None + + warning_matches: ( + list[SessionsRetrieveDecisionResponseAmlScreeningsHitsWarningMatches] | None + ) = None + """List of warning and regulatory enforcement matches. Includes entries from categories: Warning, SIP, Warnings and Regulatory Enforcement, Fitness and Probity, SIE, and Insolvency.""" + + sanction_matches: ( + list[SessionsRetrieveDecisionResponseAmlScreeningsHitsSanctionMatches] | None + ) = None + """List of sanction list matches found for this entity.""" + + adverse_media_details: ( + SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaDetails | None + ) = None + + adverse_media_matches: ( + list[SessionsRetrieveDecisionResponseAmlScreeningsHitsAdverseMediaMatches] | None + ) = None + + additional_information: dict[str, object] | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsScreenedData(BaseModel): + full_name: str | None = None + + nationality: str | None = None + + date_of_birth: _dt.date | None = None + + document_number: str | None = None + + +class SessionsRetrieveDecisionResponseAmlScreeningsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Within an AML screening this is typically `AML`, but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseAmlScreenings(BaseModel): + node_id: str | None = None + """Unique identifier for this AML step in the workflow graph.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this AML screening. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + total_hits: int | None = None + """Total number of matches found""" + + entity_type: str | None = None + """Type of entity screened: person or company""" + + hits: list[SessionsRetrieveDecisionResponseAmlScreeningsHits] | None = None + + score: float | None = None + """Overall AML screening score - the highest risk score across all hits (float, 0-100). `null` until computed.""" + + screened_data: SessionsRetrieveDecisionResponseAmlScreeningsScreenedData | None = None + + is_ongoing_monitoring_enabled: bool | None = None + """Whether AML ongoing monitoring is enabled for this screening. When `true`, the entity is re-screened periodically and any new hits update the report.""" + + next_ongoing_monitoring_bill_date: _dt.date | None = None + """Date (YYYY-MM-DD) when this ongoing-monitoring subscription will next be billed. Computed as the date of the last ongoing-monitoring billing plus 365 days when `is_ongoing_monitoring_enabled = true`; otherwise `null`. This is a billing date, not a screening date - use `last_ongoing_screening_at` for that.""" + + last_ongoing_screening_at: _dt.datetime | None = None + """When ongoing monitoring last completed a re-screening of this record, whether or not the result changed. `null` when the record has never been re-screened; it is never back-filled from the enablement or billing date.""" + + last_ongoing_screening_datasets_updated_at: _dt.datetime | None = None + """The most recent AML dataset update date the provider reported at the time of that screening. `null` when the provider reported no dataset dates; never estimated.""" + + warnings: list[SessionsRetrieveDecisionResponseAmlScreeningsWarnings] | None = None + + +class SessionsRetrieveDecisionResponseContactDetails(BaseModel): + """Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided.""" + + email: str | None = None + """Email address of the user (e.g., "john.doe@example.com") that will be used during the Email Verification step. If not provided, the user must provide it during the verification flow.""" + + email_lang: str | None = None + """Language code (ISO 639-1) for email notifications. Controls the language of all email communications (e.g., "en", "es", "fr").""" + + send_notification_emails: bool | None = None + """If true and an email is provided, Didit sends the initial "Verify your identity" email asynchronously when the User Verification (KYC) or Business Verification (KYB) session is created. Didit also sends verification status notifications for sessions requiring manual review to the provided email address.""" + + phone: str | None = None + """Phone number of the user in E.164 format if available (e.g., +14155552671). `null` when only an email was supplied at session creation. Note: This field returns the original phone number provided during session creation. If the number was invalid, it may not be in E.164 format.""" + + +class SessionsRetrieveDecisionResponseDatabaseValidationsErrors(BaseModel): + """The upstream service's error object spread as-is - its keys vary by service (commonly a message/code and any provider-specific details) - plus `service_id` identifying the service that errored.""" + + service_id: str | None = None + """Identifier of the registry service that errored.""" + + +class SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData(BaseModel): + """The input data used for validation. Fields vary by country - see the Database Validation docs (`/core-technology/database-validation/overview`) for country-specific formats.""" + + last_name: str | None = None + + first_name: str | None = None + + tax_number: str | None = None + """Brazil: CPF (11 digits)""" + + personal_number: str | None = None + """Chile: RUT, DOM: Cédula (11 digits), ECU: Cédula (10 digits), ESP: DNI/NIE, MEX: CURP (18 chars), PER: DNI (8 digits), URY: CI""" + + document_number: str | None = None + """ARG: DNI, BOL: CI, COL: Cédula, GTM: DPI, HND: DNI, PAN: Cédula, PRY: CI, SLV: DUI, VEN: Cédula""" + + date_of_birth: _dt.date | None = None + + document_type: str | None = None + """COL: CC/CE/TI/NIT, ESP: ID/PASSPORT""" + + expiration_date: _dt.date | None = None + """Required for ESP""" + + +class SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation(BaseModel): + """The results of the field validations.""" + + full_name: str | None = None + """Match result for full name (e.g., 'full_match', 'partial_match', 'no_match').""" + + date_of_birth: str | None = None + """Match result for date of birth.""" + + identification_number: str | None = None + """Match result for identification number.""" + + +class SessionsRetrieveDecisionResponseDatabaseValidationsValidations(BaseModel): + service_id: str | None = None + """Identifier of the registry service that produced this validation result.""" + + service_name: str | None = None + """Human-readable name of the registry service.""" + + validation: SessionsRetrieveDecisionResponseDatabaseValidationsValidationsValidation | None = ( + None + ) + """The results of the field validations.""" + + outcome_code: str | None = None + """Machine-readable outcome of the registry lookup (e.g. `MATCH`, `NO_MATCH`, `PARTIAL_MATCH`, `INCONCLUSIVE`, source-specific codes). Present when the service reports one.""" + + outcome_detail: str | None = None + """Human-readable explanation of `outcome_code`, when provided by the service.""" + + source_data: dict[str, object] | None = None + """The data returned from the government database. Common fields: identification_number, first_name, last_name, full_name, date_of_birth, gender. Additional fields by country: BRA always includes lgpd_minor, minor_under_18, and minor_under_16 on successful CPF matches; it also includes the upstream HTTP status when CPF data is withheld under LGPD (HTTP 422/451). ECU adds street, formatted_address, nationality, education, marital_status, spouse, mother_name, father_name, profession. PER adds paternal_name, maternal_name, verification_number, verification_letter. ESP adds document_type, expiration_date. VEN/COL add document_type.""" + + +class SessionsRetrieveDecisionResponseDatabaseValidationsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseDatabaseValidations(BaseModel): + node_id: str | None = None + """Unique identifier for this database validation step in the workflow graph.""" + + issuing_state: str | None = None + """ISO 3166-1 alpha-3 code of the country whose government/registry sources were queried (e.g. `BRA`, `MEX`, `ESP`, `NGA`, `IDN`). Database Validation covers 60+ countries across the Americas, Europe, Africa, the Middle East and Asia-Pacific, plus global multi-country sources - coverage expands regularly, so treat the value as an open set rather than a fixed enum.""" + + validation_type: Literal["one_by_one", "two_by_two", "not_enabled"] | str | None = None + """How the result was corroborated: `one_by_one` (single-service outcome), `two_by_two` (two or more distinct services independently full-matched the identification number), or `not_enabled` (persisted when no service produced a usable match). The value is never `null`. Dedicated biometric 2x2 validation modes are currently available for `ECU` and `PER`.""" + + screened_data: SessionsRetrieveDecisionResponseDatabaseValidationsScreenedData | None = None + """The input data used for validation. Fields vary by country - see the Database Validation docs (`/core-technology/database-validation/overview`) for country-specific formats.""" + + validations: list[SessionsRetrieveDecisionResponseDatabaseValidationsValidations] | None = None + """One entry per registry service that returned a validation payload. Each entry identifies the service (`service_id`, `service_name`) and carries the per-field `validation` comparison, the service `outcome_code` / `outcome_detail` when reported, and the normalised registry record in `source_data`.""" + + errors: list[SessionsRetrieveDecisionResponseDatabaseValidationsErrors] | None = None + """Errors returned by registry services while validating, each tagged with the `service_id` that produced it. **This key is omitted entirely when there are no errors.**""" + + match_type: Literal["no_match", "partial_match", "full_match"] | str | None = None + """The aggregate match outcome across the queried services. `null` until the validation has been evaluated.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this database validation. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + warnings: list[SessionsRetrieveDecisionResponseDatabaseValidationsWarnings] | None = None + """Warnings related to the database validation.""" + + +class SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields(BaseModel): + key: str | None = None + """Stable field key used as the key in `extracted_data`.""" + + name: str | None = None + """Human-readable field name.""" + + instruction: str | None = None + """Extraction instruction shown to the model.""" + + type: Literal["text", "number", "date"] | str | None = None + """Expected value type of the extracted field.""" + + required: bool | None = None + """Whether this field is required.""" + + +class SessionsRetrieveDecisionResponseDocumentAiDocumentsItems(BaseModel): + uuid: str | None = None + + node_id: str | None = None + """Workflow graph node that produced this document.""" + + document_key: str | None = None + """Key of the configured Document AI document this file corresponds to.""" + + original_filename: str | None = None + """Filename as uploaded by the user.""" + + file_size: int | None = None + """File size in bytes.""" + + file_url: str | None = None + """Short-lived presigned link to the uploaded file.""" + + status: Literal["Approved", "Declined", "In Review", "Not Finished"] | str | None = None + """Per-document Document AI status.""" + + extracted_data: dict[str, object] | None = None + """Values extracted by Document AI, keyed by the configured field `key`. Each value is a string, number, or `null`.""" + + detected_codes: list[DocumentAIDetectedCode] | None = None + """QR codes and barcodes detected across every processed page. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.""" + + fields: list[SessionsRetrieveDecisionResponseDocumentAiDocumentsItemsFields] | None = None + """The configured extraction fields for this document.""" + + document_metadata: dict[str, object] | None = None + """File-forensics metadata extracted from the uploaded document (e.g. `is_tampered`). `null` when nothing could be extracted.""" + + cross_check_result: dict[str, object] | None = None + """Cross-reference of the extracted document fields against other session data. `null` when no cross-check was performed.""" + + created_at: _dt.datetime | None = None + + +class SessionsRetrieveDecisionResponseDocumentAiDocuments(BaseModel): + """Document AI results grouped by workflow node.""" + + status: Literal["Approved", "Declined", "In Review", "Not Finished"] | str | None = None + """Aggregate Document AI status for this node, rolled up from its `items[]`.""" + + node_id: str | None = None + """Unique identifier for this Document AI step in the workflow graph.""" + + items: list[SessionsRetrieveDecisionResponseDocumentAiDocumentsItems] | None = None + + warnings: list[dict[str, object]] | None = None + """Warning logs raised for this Document AI node.""" + + +class SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue(BaseModel): + total: int | None = None + + approved: int | None = None + + pending: int | None = None + + declined: int | None = None + + in_review: int | None = None + + other: int | None = None + + missing: int | None = None + """Requested placeholders with no file uploaded yet.""" + + +class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions( + BaseModel +): + page: int | None = None + + x: float | None = None + + y: float | None = None + + width: float | None = None + + height: float | None = None + + page_width: float | None = None + + page_height: float | None = None + + +class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation( + BaseModel +): + """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" + + detected: bool | None = None + """Whether overlay-text manipulation evidence was detected.""" + + analyzed: bool | None = None + """Whether the PDF could be analyzed.""" + + signals: list[str] | None = None + """Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation.""" + + manipulated_regions: ( + list[ + SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulationManipulatedRegions + ] + | None + ) = None + """Suspected manipulated areas in PDF page coordinates.""" + + +class SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata(BaseModel): + """Metadata extracted from the uploaded corporate document. When metadata extraction succeeded, all 16 keys are always present (individual values are `null` when not applicable to the file type); the whole object is `null` when nothing could be extracted. For PDFs, `overlay_manipulation` carries forensic overlay-text analysis.""" + + file_size: int | None = None + """Size of the document.""" + + content_type: str | None = None + """Content type of the document.""" + + creation_date: str | None = None + """Date when the document was created.""" + + modified_date: str | None = None + """Date when the document was modified.""" + + creator: str | None = None + """Authoring tool/user recorded in the file metadata (PDF `Creator`).""" + + producer: str | None = None + """Software that produced the file (PDF `Producer`).""" + + software: str | None = None + """Editing software recorded in the file metadata.""" + + encryption: str | None = None + """Encryption scheme of the file, when encrypted.""" + + is_signed: bool | None = None + """Whether the document carries a digital signature.""" + + is_tampered: bool | None = None + """Whether the digital signature failed validation (post-signing modification).""" + + signature_info: dict[str, object] | None = None + """Details of the digital signature(s), when present.""" + + exif_original_date: str | None = None + """EXIF original capture date, for image uploads.""" + + exif_digitized_date: str | None = None + """EXIF digitization date, for image uploads.""" + + processed_by_known_editor: bool | None = None + """Whether metadata indicates the file passed through a known image/PDF editor.""" + + has_different_creation_mod_date: bool | None = None + """Whether creation and modification dates differ - a possible (but not conclusive) editing signal.""" + + overlay_manipulation: ( + SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadataOverlayManipulation + | None + ) = None + """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" + + +class SessionsRetrieveDecisionResponseDocumentVerificationsItems(BaseModel): + uuid: str | None = None + + document_type: ( + Literal[ + "LEGAL_PRESENCE", + "COMPANY_DETAILS", + "OWNERSHIP_STRUCTURE", + "REPRESENTATIVES_AUTHORIZATION", + "OTHER", + ] + | str + | None + ) = None + """High-level KYB document group the file was classified into. `null` while unclassified.""" + + document_subtype: str | None = None + """Specific OCR-detected subtype (e.g. `CERTIFICATE_OF_INCORPORATION`, `SHAREHOLDER_REGISTRY`); `UNKNOWN` when the OCR could not confidently classify the document.""" + + document_group: ( + Literal[ + "LEGAL_PRESENCE", + "COMPANY_DETAILS", + "OWNERSHIP_STRUCTURE", + "REPRESENTATIVES_AUTHORIZATION", + "OTHER", + ] + | str + | None + ) = None + """Alias of `document_type` - the group name used by `groups` and `required_groups`.""" + + file_url: str | None = None + """Short-lived presigned link to the uploaded file. `null` for analyst-created document request placeholders (`is_requested = true`) that have not been fulfilled yet.""" + + original_filename: str | None = None + """Filename as uploaded by the user or analyst.""" + + file_size: int | None = None + """File size in bytes.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Per-document verification status.""" + + created_at: _dt.datetime | None = None + + cross_check_result: dict[str, object] | None = None + """Cross-reference of the OCR-extracted document fields against the registry company profile. An object with `critical`, `non_critical` and `people` sections, each mapping a field name (e.g. `company_name`, `registration_number`, `incorporation_date`, `registered_address`) to a comparison record like `{"document": "...", "registry": "...", "match": true, "score": 0.97}`. `null` when the document had no OCR data or there was no registry company to compare against.""" + + document_metadata: ( + SessionsRetrieveDecisionResponseDocumentVerificationsItemsDocumentMetadata | None + ) = None + """Metadata extracted from the uploaded corporate document. When metadata extraction succeeded, all 16 keys are always present (individual values are `null` when not applicable to the file type); the whole object is `null` when nothing could be extracted. For PDFs, `overlay_manipulation` carries forensic overlay-text analysis.""" + + description: str | None = None + """Analyst-provided label or reason for a document request. `null` for regular uploads.""" + + is_requested: bool | None = None + """True for analyst-created document request placeholders awaiting an upload.""" + + ocr_data: dict[str, object] | None = None + + is_console_upload: bool | None = None + """True for documents uploaded from the console rather than the end-user flow. Console uploads never change the feature-level aggregate status.""" + + is_confirmed: bool | None = None + """Console uploads start `false` and flip `true` once the admin confirms them; end-user uploads are always `true`.""" + + +class SessionsRetrieveDecisionResponseDocumentVerifications(BaseModel): + """Documents grouped by workflow node. Present only when session_kind = business.""" + + status: str | None = None + + node_id: str | None = None + + items: list[SessionsRetrieveDecisionResponseDocumentVerificationsItems] | None = None + + groups: dict[str, SessionsRetrieveDecisionResponseDocumentVerificationsGroupsValue] | None = ( + None + ) + """Per-group progress counters keyed by group name (e.g. legal_presence, ownership_structure).""" + + required_groups: list[str] | None = None + + warnings: list[dict[str, object]] | None = None + + +class SessionsRetrieveDecisionResponseEmailVerificationsBreaches(BaseModel): + name: str | None = None + """Name of the breach""" + + domain: str | None = None + """Domain affected by the breach""" + + logo_path: str | None = None + """URL to the logo of the breached service""" + + breach_date: _dt.date | None = None + """Date when the breach occurred""" + + description: str | None = None + """Description of the breach""" + + is_verified: bool | None = None + """Whether the breach is verified""" + + data_classes: list[str] | None = None + """Types of data exposed in the breach""" + + breach_emails_count: int | None = None + """Number of email addresses affected in the breach""" + + +class SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails(BaseModel): + """Event-specific payload. Send events carry `status`, `reason`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `EMAIL_VERIFICATION_APPROVED`).""" + + status: str | None = None + + reason: str | None = None + + code_tried: str | None = None + + +class SessionsRetrieveDecisionResponseEmailVerificationsLifecycle(BaseModel): + type: str | None = None + + timestamp: _dt.datetime | None = None + + details: SessionsRetrieveDecisionResponseEmailVerificationsLifecycleDetails | None = None + """Event-specific payload. Send events carry `status`, `reason`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `EMAIL_VERIFICATION_APPROVED`).""" + + fee: float | None = None + + +class SessionsRetrieveDecisionResponseEmailVerificationsMatches(BaseModel): + session_id: str | None = None + + session_number: int | None = None + + vendor_data: str | None = None + + verification_date: _dt.datetime | None = None + + email: str | None = None + + status: str | None = None + + is_blocklisted: bool | None = None + + api_service: str | None = None + + source: Literal["session", "list_entry"] | str | None = None + """Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) - see the management API lists overview at `/management-api/lists/overview` for how to manage those lists.""" + + +class SessionsRetrieveDecisionResponseEmailVerificationsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseEmailVerifications(BaseModel): + node_id: str | None = None + """Unique identifier for this email step in the workflow graph.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Expired"] | str | None = ( + None + ) + """Feature-level status of this email verification step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + email: str | None = None + """The email address being verified""" + + is_breached: bool | None = None + """Indicates if the email was found in known data breaches""" + + breaches: list[SessionsRetrieveDecisionResponseEmailVerificationsBreaches] | None = None + """List of known breaches this email was found in""" + + is_disposable: bool | None = None + """Indicates if the email is from a disposable provider""" + + is_undeliverable: bool | None = None + """Indicates if the email address is undeliverable""" + + verification_attempts: int | None = None + """Number of verification attempts for this email""" + + verified_at: _dt.datetime | None = None + """Timestamp when the email was verified""" + + warnings: list[SessionsRetrieveDecisionResponseEmailVerificationsWarnings] | None = None + """List of warnings related to email verification""" + + lifecycle: list[SessionsRetrieveDecisionResponseEmailVerificationsLifecycle] | None = None + """Chronological list of events in the email verification lifecycle.""" + + matches: list[SessionsRetrieveDecisionResponseEmailVerificationsMatches] | None = None + """Matches found for this email address. Each match has a `source`: `session` (another verification session in your application matched on the same email, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the email is on an org-managed allow/block list - see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.""" + + +class SessionsRetrieveDecisionResponseExpectedDetails(BaseModel): + """Expected details supplied at session creation - used for cross-validation against extracted data (KYC) or to pre-fill the company registry search (KYB). Echoes back exactly the keys that were provided; `null` when none were supplied. KYC sessions carry the person fields; Business (KYB) sessions carry `company_name`, `registry_country` and `registration_number`.""" + + first_name: str | None = None + """User's first name. For example, `John`.""" + + last_name: str | None = None + """User's last name. For example, `Doe`.""" + + date_of_birth: _dt.date | None = None + """User's date of birth with format: YYYY-MM-DD. For example, `1990-05-15`.""" + + gender: Literal["M", "F"] | str | None = None + """User's gender. Must be either 'M', 'F', or null.""" + + nationality: str | None = None + """ISO 3166-1 alpha-3 country code representing the applicant's country of origin. For example, `USA`.""" + + country: str | None = None + """ISO 3166-1 alpha-3 country code representing the country of the Proof of Address document, or the country of the applicant's ID document, which may differ from nationality. For example, `GBR`.""" + + address: str | None = None + """The address in a human readable format, including as much information as possible. For example, `123 Main St, San Francisco, CA 94105, USA`.""" + + identification_number: str | None = None + """The user's document number, personal number, or tax number. For example, `123456789`.""" + + ip_address: str | None = None + """Expected IP address for the session. If the actual IP address differs from this value, a warning will be logged. For example, `192.168.1.100`.""" + + expected_document_types: ( + list[Literal["P", "ID", "DL", "RP", "HIC", "TC", "SSC"] | str] | None + ) = None + """Document types the ID verification step was restricted to at session creation. Empty/unset means no restriction. Codes: `P` (passport), `ID` (national ID), `DL` (driver's license), `RP` (residence permit), `HIC` (health insurance card), `TC` (tax card), `SSC` (social security card).""" + + id_country: str | None = None + """Expected ID-document issuing country (ISO 3166-1 alpha-3), when restricted at creation time.""" + + poa_country: str | None = None + """Expected Proof of Address document country (ISO 3166-1 alpha-3), when restricted at creation time.""" + + company_name: str | None = None + """Expected legal name of the business. Business (KYB) sessions only.""" + + registry_country: str | None = None + """Expected registry country as `XX` or `XX-YY` (ISO 3166-1 alpha-2, optionally with an ISO 3166-2 subdivision for state-level registries, e.g. `US-CA`). Business (KYB) sessions only.""" + + registration_number: str | None = None + """Expected company registration number. Business (KYB) sessions only.""" + + +class SessionsRetrieveDecisionResponseFaceMatchesWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseFaceMatches(BaseModel): + """One face-match report - the full report.""" + + node_id: str | None = None + """Workflow graph node id of the face-match step that produced this report.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this face-match step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + score: float | None = None + """Confidence score of the face match""" + + source_image_session_id: str | None = None + """Session id under which the source (reference) portrait is stored. In biometric-authentication flows this is the session's own id (the submitted portrait is stored under the new session). `null` when the source image is the same session's document portrait.""" + + source_image: str | None = None + """URL of the source image used for comparison""" + + target_image: str | None = None + """URL of the target image used for comparison""" + + warnings: list[SessionsRetrieveDecisionResponseFaceMatchesWarnings] | None = None + """Face match verification warnings""" + + +class SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore(BaseModel): + """Quality metrics for the back document image. Same structure as front_image_quality_score.""" + + focus_score: float | None = None + """Sharpness of the image measured via top-10% local Laplacian variance. Robust to documents with large uniform backgrounds. Higher values indicate a sharper, less blurry image.""" + + brightness_score: float | None = None + """How well-lit the image is. Penalizes images that are too dark or overexposed, with ideal brightness around mid-range.""" + + brightness_issue: Literal["ok", "too_dark", "too_bright"] | str | None = None + """Indicates the direction of the brightness problem when the brightness_score is low. 'ok' means brightness is acceptable, 'too_dark' means the image is underexposed, 'too_bright' means the image is overexposed.""" + + is_document_fully_visible: bool | None = None + """true if all four corners of the document are fully visible inside the image frame, false if any corner is cut off. Null when corner coordinates are unavailable.""" + + resolution_score: float | None = None + """Effective resolution of the image based on total pixel count.""" + + overall_score: float | None = None + """Weighted composite of sub-scores (focus 45%, brightness 30%, resolution 25%).""" + + +class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace(BaseModel): + score: float | None = None + """Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score.""" + + front: float | None = None + """Score for the front side; `null` when not captured or when the score was dropped.""" + + back: float | None = None + """Score for the back side; `null` when not captured or when the score was dropped.""" + + decline_threshold: float | None = None + """The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable.""" + + review_threshold: float | None = None + """The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable.""" + + bucket: Literal["approve", "review", "decline"] | str | None = None + """The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy(BaseModel): + score: float | None = None + """Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score.""" + + front: float | None = None + """Score for the front side; `null` when not captured or when the score was dropped.""" + + back: float | None = None + """Score for the back side; `null` when not captured or when the score was dropped.""" + + decline_threshold: float | None = None + """The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable.""" + + review_threshold: float | None = None + """The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable.""" + + bucket: Literal["approve", "review", "decline"] | str | None = None + """The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay(BaseModel): + score: float | None = None + """Likelihood that the document is genuine for this fraud type, 0-100 (higher = more likely genuine). The lowest of the two sides - the value the decision used. `null` when neither side produced a usable score.""" + + front: float | None = None + """Score for the front side; `null` when not captured or when the score was dropped.""" + + back: float | None = None + """Score for the back side; `null` when not captured or when the score was dropped.""" + + decline_threshold: float | None = None + """The decline threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable.""" + + review_threshold: float | None = None + """The review threshold saved when this document was evaluated. Later workflow edits do not change it. Null when historical evaluation evidence is unavailable.""" + + bucket: Literal["approve", "review", "decline"] | str | None = None + """The fraud-type bucket saved at evaluation time. Null when there is no usable score, the check was not applied, or historical evaluation evidence is unavailable. Later workflow edits do not change it.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness(BaseModel): + """Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run.""" + + portrait_replace: ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPortraitReplace | None + ) = None + + printed_copy: ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessPrintedCopy | None + ) = None + + screen_replay: ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentLivenessScreenReplay | None + ) = None + + +class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails( + BaseModel +): + """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" + + full_name: str | None = None + + document_type: str | None = None + + document_number: str | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches(BaseModel): + session_id: str | None = None + """Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`.""" + + session_number: int | None = None + + similarity_percentage: float | None = None + """Face similarity between the two images, clamped to 0–100.""" + + vendor_data: str | None = None + """`vendor_data` of the matching session or linked User for imported and retained-template faces.""" + + vendor_user_id: str | None = None + """Linked User id for `imported` and `retained_template` matches and for session matches linked to a User. `null` for list entries and sessions without a User.""" + + biometric_template_id: str | None = None + """Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise.""" + + verification_date: _dt.datetime | None = None + """When the matching face was captured, or when a retained template was created. `null` for list entries.""" + + user_details: ( + SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatchesUserDetails | None + ) = None + """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" + + match_image_url: str | None = None + """Reference image for image-backed matches. Always `null` for `retained_template` matches because retained templates contain no image.""" + + status: str | None = None + """Lifecycle status of the matching session. `null` for non-session sources.""" + + is_blocklisted: bool | None = None + """Whether the matching face is on a blocklist.""" + + is_allowlisted: bool | None = None + """Whether the matching face is on an allowlist.""" + + api_service: str | None = None + """Standalone API service of the matching session (e.g. `PASSIVE_LIVENESS`, `FACE_MATCH`); `null` for workflow sessions and non-session sources.""" + + source: Literal["session", "imported", "list_entry", "retained_template"] | str | None = None + """Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`).""" + + +class SessionsRetrieveDecisionResponseIdVerificationsExtraFields(BaseModel): + """Additional document fields. Includes document-specific attributes (e.g. `dl_categories`, `blood_group`, `sponsor`, `employer`, `dl_class_code_b_from`, `dl_class_code_b_to`). Driver license class validity dates are returned as `dl_class_code__from` and `dl_class_code__to`, and per-class restriction/information codes (e.g. column 12 of UK licences) as `dl_class_code__notes`. When the document carries names, address, or place of birth in multiple scripts, this object also includes alternate-script versions of those fields. The workflow setting `preferred_characters` (`latin` or `non_latin`) controls which script populates the top-level fields (`first_name`, `last_name`, `full_name`, `address`, `place_of_birth`); the opposite script lands here as `first_name_non_latin` / `last_name_non_latin` / `full_name_non_latin` / `middle_name_non_latin` / `address_non_latin` / `place_of_birth_non_latin` (when `preferred_characters` is `latin`) or with a `_latin` suffix (when `preferred_characters` is `non_latin`). Keys appear only when the OCR actually extracts a real alternate-script value - a field whose alternate value is in the same script as the preferred set (for example, a Cyrillic-labelled field whose OCR value came back in Latin homoglyphs) is skipped. When `preferred_characters` is `latin` but the document only carries non-Latin text, Didit transliterates the extracted values and exposes `first_name_latin` / `last_name_latin` / etc. as a fallback.""" + + dl_categories: list[str] | None = None + + blood_group: str | None = None + + first_name_non_latin: str | None = None + """First name in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document (e.g. `Айна` for a Kyrgyz ID).""" + + last_name_non_latin: str | None = None + """Surname in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document.""" + + middle_name_non_latin: str | None = None + """Middle name / patronymic in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document.""" + + full_name_non_latin: str | None = None + """Composed full name in the non-Latin script. Reconstructed from the individual components when both first and last names are available.""" + + address_non_latin: str | None = None + """Address in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document.""" + + place_of_birth_non_latin: str | None = None + """Place of birth in the non-Latin script when `preferred_characters=latin` and a non-Latin version is present on the document.""" + + first_name_latin: str | None = None + """First name in Latin script. Populated when `preferred_characters=non_latin` and a Latin version is present on the document, or - as a transliteration fallback - when `preferred_characters=latin` but the document only carries non-Latin text.""" + + last_name_latin: str | None = None + """Surname in Latin script. Same rules as `first_name_latin`.""" + + middle_name_latin: str | None = None + """Middle name / patronymic in Latin script. Same rules as `first_name_latin`.""" + + full_name_latin: str | None = None + """Composed full name in Latin script. Always reconstructed from the individual components when both first and last names are available.""" + + address_latin: str | None = None + """Address in Latin script. Same rules as `first_name_latin`.""" + + place_of_birth_latin: str | None = None + """Place of birth in Latin script. Same rules as `first_name_latin`.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom(BaseModel): + """Why a non-document method ended the ID step. Set on the declined report when a lookup or a wallet sign-in failed and the country's fallback switch was `decline`. `null` otherwise.""" + + method: Literal["id_lookup", "wallet"] | str | None = None + """The method that failed.""" + + reason: str | None = None + """The outcome that ended it, e.g. `no_match`, `provider_error`, `cancelled`, `timeout`.""" + + action: str | None = None + """The configured action that was taken, e.g. `decline`.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore(BaseModel): + """Quality metrics for the front document image. Each sub-score ranges from 0 to 100.""" + + focus_score: float | None = None + """Sharpness of the image measured via top-10% local Laplacian variance. Robust to documents with large uniform backgrounds. Higher values indicate a sharper, less blurry image.""" + + brightness_score: float | None = None + """How well-lit the image is. Penalizes images that are too dark or overexposed, with ideal brightness around mid-range.""" + + brightness_issue: Literal["ok", "too_dark", "too_bright"] | str | None = None + """Indicates the direction of the brightness problem when the brightness_score is low. 'ok' means brightness is acceptable, 'too_dark' means the image is underexposed, 'too_bright' means the image is overexposed.""" + + is_document_fully_visible: bool | None = None + """true if all four corners of the document are fully visible inside the image frame, false if any corner is cut off. Null when corner coordinates are unavailable.""" + + resolution_score: float | None = None + """Effective resolution of the image based on total pixel count.""" + + overall_score: float | None = None + """Weighted composite of sub-scores (focus 45%, brightness 30%, resolution 25%).""" + + +class SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison(BaseModel): + field: ( + Literal["identification_number", "full_name", "date_of_birth", "address", "provider"] | str + ) + """Normalised comparison field. `provider` is not a user field: it marks a source error row.""" + + label: str + """Display label for the row, e.g. `ID number`, `Full name`, `Provider`.""" + + provided: str | None + """What the user typed. `null` on a `provider` error row.""" + + record: str | None + """What the source held. On a `provider` error row this carries the error message instead.""" + + result: Literal["match", "partial", "no_match", "error"] | str + """Per-field comparison result. `error` appears only on a `provider` row and means that source did not answer; treat it as evidence about the query, never as a field mismatch.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsIdLookup(BaseModel): + """Registry evidence. Present only when `verification_method` is `id_lookup`; `null` otherwise.""" + + source: str | None = None + """Customer-facing name of the source queried, e.g. `Department of Home Affairs` (a government register) or `UK credit bureau and financial services records` (not a government register). This is the only statement of provenance for the lookup; do not assume a government source.""" + + checked_at: _dt.datetime | None = None + """When the register answered.""" + + attempts: int | None = None + """1-based attempt number this report came from.""" + + max_attempts: int | None = None + """Attempt budget configured for the country (1 to 5, default 1).""" + + outcome: ( + Literal["match", "partial_match", "no_match", "provider_error", "format_rejected"] + | str + | None + ) = None + """What the register answered. Only `match`, `partial_match` and `no_match` count against the attempt budget and bill.""" + + comparison: list[SessionsRetrieveDecisionResponseIdVerificationsIdLookupComparison] | None = ( + None + ) + """One row per comparable field the source answered on, plus one row per source error. Comparison rows use the normalised field names below rather than the country's request field keys, because several request fields (first and last name, or any of the identifier aliases) collapse into a single comparison. A source that failed appends a row with `field: "provider"` and `result: "error"`, so read `result` before treating a row as a field verdict.""" + + registry_portrait: str | None = None + """Short-lived presigned URL to the portrait the source returned. Only the four government registers that return one (South Africa, Nigeria, Argentina, Panama).""" + + face_match_score: float | None = None + """Selfie against the registry portrait, 0-100. Only where the register returns a portrait.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails(BaseModel): + name: str | None = None + """Full name from the matching document""" + + document_type: str | None = None + """Type of the matching document""" + + document_number: str | None = None + """Document number of the matching document""" + + +class SessionsRetrieveDecisionResponseIdVerificationsMatches(BaseModel): + session_id: str | None = None + """Session ID of the matching document""" + + session_number: int | None = None + """Session number of the matching document""" + + vendor_data: str | None = None + """Vendor data associated with the matching session""" + + verification_date: _dt.datetime | None = None + """Date when the matching session was verified""" + + user_details: SessionsRetrieveDecisionResponseIdVerificationsMatchesUserDetails | None = None + + status: str | None = None + """Status of the matching session""" + + is_blocklisted: bool | None = None + """Whether the matching document is blocklisted""" + + api_service: str | None = None + """API service type of the matching session""" + + front_image_url: str | None = None + """URL of the front document image from the matching document""" + + +class SessionsRetrieveDecisionResponseIdVerificationsMrz(BaseModel): + """Parsed Machine-Readable Zone (MRZ) fields extracted from the document. `null` if no MRZ was detected.""" + + surname: str | None = None + + name: str | None = None + + country: str | None = None + + nationality: str | None = None + + birth_date: str | None = None + + expiry_date: str | None = None + + sex: str | None = None + + document_type: str | None = None + + document_number: str | None = None + + optional_data: str | None = None + + optional_data_2: str | None = None + + birth_date_hash: str | None = None + + expiry_date_hash: str | None = None + + document_number_hash: str | None = None + + final_hash: str | None = None + + personal_number: str | None = None + + warnings: list[str] | None = None + + errors: list[str] | None = None + + mrz_type: str | None = None + + mrz_string: str | None = None + + mrz_key: str | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation(BaseModel): + latitude: float | None = None + + longitude: float | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents( + BaseModel +): + types: list[str] | None = None + + long_name: str | None = None + + short_name: str | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation( + BaseModel +): + lat: float | None = None + + lng: float | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast( + BaseModel +): + lat: float | None = None + + lng: float | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest( + BaseModel +): + lat: float | None = None + + lng: float | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport( + BaseModel +): + northeast: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportNortheast + | None + ) = None + + southwest: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewportSouthwest + | None + ) = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry(BaseModel): + location: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryLocation + | None + ) = None + + viewport: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometryViewport + | None + ) = None + + location_type: str | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation( + BaseModel +): + latitude: float | None = None + + longitude: float | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints( + BaseModel +): + location: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPointsLocation + | None + ) = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults(BaseModel): + types: list[str] | None = None + + geometry: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsGeometry | None + ) = None + + place_id: str | None = None + + formatted_address: str | None = None + + navigation_points: ( + list[SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsNavigationPoints] + | None + ) = None + + address_components: ( + list[ + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResultsAddressComponents + ] + | None + ) = None + + +class SessionsRetrieveDecisionResponseIdVerificationsParsedAddress(BaseModel): + """Structured address details""" + + city: str | None = None + + label: str | None = None + + region: str | None = None + + country: str | None = None + """Two-letter country code""" + + category: str | None = None + """Address category (e.g., Residential, Commercial)""" + + street_1: str | None = None + + street_2: str | None = None + + is_verified: bool | None = None + """Whether the address has been verified""" + + postal_code: str | None = None + + raw_results: SessionsRetrieveDecisionResponseIdVerificationsParsedAddressRawResults | None = ( + None + ) + + address_type: str | None = None + + document_location: ( + SessionsRetrieveDecisionResponseIdVerificationsParsedAddressDocumentLocation | None + ) = None + + formatted_address: str | None = None + + +class SessionsRetrieveDecisionResponseIdVerificationsWalletVerification(BaseModel): + """Credential evidence. Present only when `verification_method` is `wallet`; `null` otherwise.""" + + provider: str | None = None + """Catalog wallet id, e.g. `mitid`.""" + + provider_name: str | None = None + """Wallet name as the end user knows it, e.g. `MitID`.""" + + issuing_authority: str | None = None + """The authority behind the wallet, e.g. `Danish Agency for Digital Government`.""" + + issuing_country: str | None = None + """ISO 3166-1 alpha-3 country the wallet was offered in.""" + + credential_type: str | None = None + """Credential the wallet presented, e.g. `person_identification`.""" + + level_of_assurance: Literal["low", "substantial", "high"] | str | None = None + """The level of assurance the wallet asserted. A sign-in that asserts less than the level requested fails instead of downgrading.""" + + verified_at: _dt.datetime | None = None + """The wallet's authentication instant.""" + + signature_valid: bool | None = None + """Whether the signed assertion the wallet returned validated.""" + + attributes: dict[str, object] | None = None + """The attributes the wallet shared, keyed as the wallet's catalog entry lists them.""" + + portrait: str | None = None + """Short-lived presigned URL to the portrait the wallet shared, where it shares one.""" + + face_match_score: float | None = None + """Selfie against the wallet portrait, 0-100, where the wallet shares one.""" + + +class SessionsRetrieveDecisionResponseIdVerificationsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning (e.g., `ID_VERIFICATION`, `NFC`, `LIVENESS`). The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module, but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + """Additional contextual data about the warning""" + + log_type: str | None = None + """Type of log: information, warning, or error""" + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning (for graph-based workflows)""" + + +class SessionsRetrieveDecisionResponseIdVerifications(BaseModel): + """One ID verification report - the full report for a single ID Verification step. `verification_method` says which of the three methods produced it (document capture, non-doc lookup or wallet sign-in) and the method-specific evidence block is populated accordingly.""" + + node_id: str | None = None + """Workflow graph node id of the OCR step that produced this report.""" + + verification_method: Literal["document", "id_lookup", "wallet"] | str | None = None + """Which ID Verification method produced this report. `document` - the user photographed a physical ID (the default, and the value on every session created before the other methods existed). `id_lookup` - the user typed their national ID number and it was checked against a government register or another authoritative source for that country (`id_lookup.source` names which). `wallet` - the user signed in with a government or bank digital identity.""" + + assurance: Literal["documentary", "data_match", "cryptographic"] | str | None = None + """Assurance tier derived from `verification_method`: `documentary` for a document that was captured and checked, `data_match` for details that matched a record held by the source queried, `cryptographic` for attributes an identity provider authenticated and signed.""" + + wallet_provider: str | None = None + """Catalog wallet id when `verification_method` is `wallet` (`mitid`, `bankid_se`, `itsme`, ...). `null` otherwise.""" + + fallback_from: SessionsRetrieveDecisionResponseIdVerificationsFallbackFrom | None = None + """Why a non-document method ended the ID step. Set on the declined report when a lookup or a wallet sign-in failed and the country's fallback switch was `decline`. `null` otherwise.""" + + id_lookup: SessionsRetrieveDecisionResponseIdVerificationsIdLookup | None = None + """Registry evidence. Present only when `verification_method` is `id_lookup`; `null` otherwise.""" + + wallet_verification: ( + SessionsRetrieveDecisionResponseIdVerificationsWalletVerification | None + ) = None + """Credential evidence. Present only when `verification_method` is `wallet`; `null` otherwise.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Expired"] | str | None = ( + None + ) + """Feature-level status of this ID verification step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + document_type: str | None = None + """Type of identity document""" + + document_subtype: str | None = None + """Accepted document subtype code from the workflow document settings, including the document region when applicable. Examples: `ID_CARD_GENERIC`, `QUEENSLAND_DRIVER_LICENSE_GENERIC`.""" + + document_number: str | None = None + """Document identification number""" + + personal_number: str | None = None + """Personal identification number""" + + portrait_image: str | None = None + """URL of the portrait image""" + + front_image: str | None = None + """URL of the front document image""" + + front_video: str | None = None + """URL of the front document video""" + + back_image: str | None = None + """URL of the back document image""" + + back_video: str | None = None + """URL of the back document video""" + + full_front_image: str | None = None + """URL of the full front document image""" + + full_back_image: str | None = None + """URL of the full back document image""" + + front_image_camera_front: str | None = None + """URL of the front document image captured from the front-facing camera""" + + back_image_camera_front: str | None = None + """URL of the back document image captured from the front-facing camera""" + + date_of_birth: _dt.date | None = None + """Date of birth from document""" + + age: int | None = None + """Calculated age from date of birth""" + + expiration_date: _dt.date | None = None + """Document expiration date""" + + date_of_issue: _dt.date | None = None + """Document issue date""" + + issuing_state: str | None = None + """ISO code of issuing state""" + + issuing_state_name: str | None = None + """Full name of issuing state""" + + first_name: str | None = None + """First name from document""" + + last_name: str | None = None + """Last name from document""" + + full_name: str | None = None + """Full name from document""" + + gender: str | None = None + """Gender from document""" + + address: str | None = None + """Raw address from document""" + + formatted_address: str | None = None + """Formatted full address""" + + place_of_birth: str | None = None + """Place of birth from document""" + + marital_status: str | None = None + """Marital status from document""" + + nationality: str | None = None + """ISO nationality code""" + + extra_fields: SessionsRetrieveDecisionResponseIdVerificationsExtraFields | None = None + """Additional document fields. Includes document-specific attributes (e.g. `dl_categories`, `blood_group`, `sponsor`, `employer`, `dl_class_code_b_from`, `dl_class_code_b_to`). Driver license class validity dates are returned as `dl_class_code__from` and `dl_class_code__to`, and per-class restriction/information codes (e.g. column 12 of UK licences) as `dl_class_code__notes`. When the document carries names, address, or place of birth in multiple scripts, this object also includes alternate-script versions of those fields. The workflow setting `preferred_characters` (`latin` or `non_latin`) controls which script populates the top-level fields (`first_name`, `last_name`, `full_name`, `address`, `place_of_birth`); the opposite script lands here as `first_name_non_latin` / `last_name_non_latin` / `full_name_non_latin` / `middle_name_non_latin` / `address_non_latin` / `place_of_birth_non_latin` (when `preferred_characters` is `latin`) or with a `_latin` suffix (when `preferred_characters` is `non_latin`). Keys appear only when the OCR actually extracts a real alternate-script value - a field whose alternate value is in the same script as the preferred set (for example, a Cyrillic-labelled field whose OCR value came back in Latin homoglyphs) is skipped. When `preferred_characters` is `latin` but the document only carries non-Latin text, Didit transliterates the extracted values and exposes `first_name_latin` / `last_name_latin` / etc. as a fallback.""" + + mrz: SessionsRetrieveDecisionResponseIdVerificationsMrz | None = None + """Parsed Machine-Readable Zone (MRZ) fields extracted from the document. `null` if no MRZ was detected.""" + + parsed_address: SessionsRetrieveDecisionResponseIdVerificationsParsedAddress | None = None + """Structured address details""" + + extra_files: list[str] | None = None + """Additional document verification files""" + + front_image_camera_front_face_match_score: float | None = None + """Face match score between the portrait extracted from the front document image and the front camera image""" + + back_image_camera_front_face_match_score: float | None = None + """Face match score between the portrait extracted from the back document image and the front camera image""" + + document_selfie_face_matches: ( + list[SessionsRetrieveDecisionResponseIdVerificationsDocumentSelfieFaceMatches] | None + ) = None + """Previous sessions or face blocklist entries whose face matched the selfie the front camera took during document capture (Silent Capture Face Search). Same entry format as `liveness_checks[].matches`. `null` for sessions created before this field existed or when the search did not run; an empty array when it ran and found nothing. `match_image_url` is a presigned URL that expires.""" + + front_image_quality_score: ( + SessionsRetrieveDecisionResponseIdVerificationsFrontImageQualityScore | None + ) = None + """Quality metrics for the front document image. Each sub-score ranges from 0 to 100.""" + + back_image_quality_score: ( + SessionsRetrieveDecisionResponseIdVerificationsBackImageQualityScore | None + ) = None + """Quality metrics for the back document image. Same structure as front_image_quality_score.""" + + warnings: list[SessionsRetrieveDecisionResponseIdVerificationsWarnings] | None = None + """Document verification warnings""" + + matches: list[SessionsRetrieveDecisionResponseIdVerificationsMatches] | None = None + """Matching documents found during duplicate/blocklist detection (up to 5 matches)""" + + document_liveness: SessionsRetrieveDecisionResponseIdVerificationsDocumentLiveness | None = None + """Document-liveness scores per fraud type, reported whether or not a warning fired, so an approved document can still be audited. `null` when the check did not run.""" + + +class SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation(BaseModel): + """Coordinates, or `null` when unavailable.""" + + latitude: float | None = None + + longitude: float | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesIdDocument(BaseModel): + """Location extracted from the ID document address plus distances (km) to the IP geolocation and the POA document address. `location` is `null` when no document address could be geocoded.""" + + location: SessionsRetrieveDecisionResponseIpAnalysesIdDocumentLocation | None = None + """Coordinates, or `null` when unavailable.""" + + distance_from_ip: float | None = None + + distance_from_poa_document: float | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesIpLocation(BaseModel): + """Coordinates, or `null` when unavailable.""" + + latitude: float | None = None + + longitude: float | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesIp(BaseModel): + """IP geolocation of this entry plus great-circle distances (km) to the ID document address and the POA document address. Distances are `null` when either side has no resolvable location.""" + + location: SessionsRetrieveDecisionResponseIpAnalysesIpLocation | None = None + """Coordinates, or `null` when unavailable.""" + + distance_from_id_document: float | None = None + + distance_from_poa_document: float | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo(BaseModel): + """Device details observed on the matching session.""" + + device_brand: str | None = None + + device_model: str | None = None + + browser_family: str | None = None + + os_family: str | None = None + + platform: str | None = None + + device_fingerprint: str | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo(BaseModel): + """Network and geolocation details observed on the matching session.""" + + ip_address: str | None = None + + ip_country: str | None = None + + ip_country_code: str | None = None + + ip_state: str | None = None + + ip_city: str | None = None + + is_vpn_or_tor: bool | None = None + + is_data_center: bool | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesMatches(BaseModel): + session_id: str | None = None + """Session ID of the matching session.""" + + session_number: int | None = None + """Sequential number of the matching session.""" + + vendor_data: str | None = None + """`vendor_data` of the matching session - always different from the current session's vendor user.""" + + verification_date: _dt.datetime | None = None + """Creation date of the matching session (`YYYY-MM-DDTHH:MM:SSZ`).""" + + match_type: Literal["ip_address", "device_fingerprint"] | str | None = None + """Whether the match was on IP address or device fingerprint""" + + match_source: ( + Literal["ip_address", "persistent_id", "legacy_fp", "recovered_high"] | str | None + ) = None + """How this match was reached: `ip_address` (shared IP address - network co-location, not a device-identity claim), `persistent_id` (exact stored persistent device identifier), `legacy_fp` (legacy `didit-fp-*` device-fingerprint hash), or `recovered_high` (high-confidence fuzzy fingerprint-recovery hit). Lower-confidence recovery candidates surface as risk warnings only, never in this array.""" + + matched_value: str | None = None + """The value that matched: the shared IP address, the persistent device identifier or legacy fingerprint hash, or - for `recovered_high` - the UUID of the recovered device.""" + + status: str | None = None + """Lifecycle status of the matching session.""" + + is_blocklisted: bool | None = None + """Whether the matched value is on a blocklist. Currently always `false` for device/IP matches.""" + + api_service: str | None = None + """Standalone API service of the matching session; `null` for workflow sessions.""" + + source: Literal["session"] | str | None = None + """Always `session` for device/IP matches - every match is another verification session in your application.""" + + device_info: SessionsRetrieveDecisionResponseIpAnalysesMatchesDeviceInfo | None = None + """Device details observed on the matching session.""" + + location_info: SessionsRetrieveDecisionResponseIpAnalysesMatchesLocationInfo | None = None + """Network and geolocation details observed on the matching session.""" + + confidence: float | None = None + """Server-side identification confidence, computed as `1 - P(false positive)`: `1.0` for deterministic matches (`persistent_id`, or hardware-rooted `recovered_high`), `0.5` for `legacy_fp`, `0.0` for `ip_address` (co-location only), and a corroboration-adjusted value below `1.0` for probabilistic recovered matches (higher when the TLS JA4 fingerprint and IP country independently agree).""" + + match_mode: Literal["deterministic", "probabilistic", "co_occurrence"] | str | None = None + """Evidence class of the match: `deterministic` (exact stored or hardware-rooted identifier that cannot collide between physical devices), `probabilistic` (fuzzy fingerprint evidence), or `co_occurrence` (shared network only, no device-identity claim).""" + + recovery_similarity: float | None = None + """Cosine similarity of the fingerprint-recovery match, rounded to 4 decimals. Present only when `match_source` is `recovered_high`.""" + + tls_ja4_corroborated: bool | None = None + """Whether the TLS JA4 fingerprints of the two observations agree - a server-observed signal the browser cannot spoof. Present only when `match_source` is `recovered_high`.""" + + recovery_gate_reason: str | None = None + """Why the recovery match passed the false-positive hard gate: `hardware_root_match` (a hardware-rooted device identifier matched exactly) or, on configurations that admit probe-only evidence, `exact_composite_plus__families` / `very_high_similarity_plus__families`. Present only when `match_source` is `recovered_high`.""" + + +class SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation(BaseModel): + """Coordinates, or `null` when unavailable.""" + + latitude: float | None = None + + longitude: float | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesPoaDocument(BaseModel): + """Location extracted from the Proof of Address document plus distances (km) to the IP geolocation and the ID document address. `location` is `null` when the session has no POA step or the address could not be geocoded.""" + + location: SessionsRetrieveDecisionResponseIpAnalysesPoaDocumentLocation | None = None + """Coordinates, or `null` when unavailable.""" + + distance_from_ip: float | None = None + + distance_from_id_document: float | None = None + + +class SessionsRetrieveDecisionResponseIpAnalysesWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseIpAnalyses(BaseModel): + node_id: str | None = None + """Unique identifier for this Device & IP Analysis step in the workflow graph.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this Device & IP Analysis entry. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + device_brand: str | None = None + """Brand of the device used""" + + device_model: str | None = None + """Model of the device used""" + + browser_family: str | None = None + """Browser family used""" + + os_family: str | None = None + """Operating system family""" + + platform: str | None = None + """Platform type (mobile, desktop, etc)""" + + device_fingerprint: str | None = None + """Unique device fingerprint identifier""" + + ip_country: str | None = None + """Country detected from IP""" + + ip_country_code: str | None = None + """Two-letter country code from IP""" + + ip_state: str | None = None + """State/region detected from IP""" + + ip_city: str | None = None + """City detected from IP""" + + latitude: float | None = None + """Latitude coordinate of IP location""" + + longitude: float | None = None + """Longitude coordinate of IP location""" + + ip_address: str | None = None + """IP address""" + + isp: str | None = None + """Internet Service Provider""" + + organization: str | None = None + """Organization associated with IP""" + + is_vpn_or_tor: bool | None = None + """Whether IP is from VPN or Tor network""" + + is_data_center: bool | None = None + """Whether IP is from a data center""" + + time_zone: str | None = None + """Time zone detected from IP""" + + time_zone_offset: str | None = None + """Time zone offset""" + + ip: SessionsRetrieveDecisionResponseIpAnalysesIp | None = None + """IP geolocation of this entry plus great-circle distances (km) to the ID document address and the POA document address. Distances are `null` when either side has no resolvable location.""" + + id_document: SessionsRetrieveDecisionResponseIpAnalysesIdDocument | None = None + """Location extracted from the ID document address plus distances (km) to the IP geolocation and the POA document address. `location` is `null` when no document address could be geocoded.""" + + poa_document: SessionsRetrieveDecisionResponseIpAnalysesPoaDocument | None = None + """Location extracted from the Proof of Address document plus distances (km) to the IP geolocation and the ID document address. `location` is `null` when the session has no POA step or the address could not be geocoded.""" + + warnings: list[SessionsRetrieveDecisionResponseIpAnalysesWarnings] | None = None + + matches: list[SessionsRetrieveDecisionResponseIpAnalysesMatches] | None = None + """Cross-session matches found for this entry's IP address or device identity. Every match is another verification session in your application (`source` is always `session`) that shares the same value, always belonging to a different vendor user / `vendor_data` than the current session, capped at 5 per collector (`device_fingerprint` sums two independently capped collectors, so up to 10 entries are possible). Every item carries `match_source`, `confidence`, `match_mode`, `device_info` and `location_info`; the keys `recovery_similarity`, `tls_ja4_corroborated` and `recovery_gate_reason` are present only when `match_source` is `recovered_high`.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories( + BaseModel +): + """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" + + sanctions: Literal["match", "clear"] | str | None = None + + pep: Literal["match", "clear"] | str | None = None + + adverse_media: Literal["match", "clear"] | str | None = None + + warnings: Literal["match", "clear"] | str | None = None + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures( + BaseModel +): + feature: str | None = None + """Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`).""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status inside the child session.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + """Beneficial owner's full name (or company name for corporate UBOs).""" + + first_name: str | None = None + + last_name: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + country_of_birth: str | None = None + + email: str | None = None + + company_name: str | None = None + + registration_number: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + + nationality: str | None = None + + designation: str | None = None + + roles: list[str] | None = None + """Role names held by this owner, e.g. `["ubo", "shareholder"]`.""" + + ownership_min_shares: str | None = None + """Lower bound of the ownership percentage as reported by the registry. Free-form string (e.g. `"25"`, `"25-50%"`); use `effective_ownership_percent` for a parsed numeric value.""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership percentage as reported by the registry. Free-form string; use `effective_ownership_percent` for a parsed numeric value.""" + + is_active: bool | None = None + + aml_status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Status of the AML screening run for this beneficial owner. `null` when no AML screening was run.""" + + kyc_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Raw lifecycle status of the child KYC session spawned for this beneficial owner. This is the full session status enum (not a reduced Approved/Declined/Pending set). `null` when no child KYC session exists.""" + + kyc_session_url: str | None = None + """Shareable URL of the child KYC session. `null` when no child session exists.""" + + kyc_session_id: str | None = None + """Session id of the child KYC session. Pass it to this same decision endpoint to fetch the person's full KYC report. `null` when no child session exists.""" + + verification_workflow_type: str | None = None + """Base workflow type of the child session's workflow (e.g. `kyc`). `null` for graph-based workflows or when no child session exists.""" + + verification_workflow_id: str | None = None + """Workflow id used by the child session. `null` when no child session exists.""" + + verification_workflow_label: str | None = None + """Human-readable label of the child session's workflow. `null` when unset.""" + + verification_workflow_features: ( + list[ + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersVerificationWorkflowFeatures + ] + | None + ) = None + """Per-feature progress of the child verification session, e.g. `[{"feature": "ID_VERIFICATION", "status": "Approved"}]`. `null` when the child session has no resolvable feature list.""" + + didit_internal_id: str | None = None + """Stable internal identifier of the verified individual behind the child session - useful for cross-session linking. `null` when no child session exists or the user has not been resolved yet.""" + + kyc_document_type: str | None = None + """Document type presented in the child KYC session (e.g. `P`, `ID`, `DL`). `null` until the child session has an ID verification result.""" + + kyc_nationality: str | None = None + """Nationality extracted from the document in the child KYC session. `null` until available.""" + + kyc_liveness_passed: bool | None = None + """Whether the child KYC session's liveness check passed. `null` when no liveness check has run.""" + + kyc_verified_at: _dt.datetime | None = None + """When the child KYC session reached `Approved`. `null` while the child session has any other status.""" + + kyc_aml_categories: ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwnersKycAmlCategories + | None + ) = None + """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" + + kyc_notification_sent_at: str | None = None + """ISO 8601 timestamp of the last verification-invite notification sent for this person's child session. `null` when none was sent.""" + + child_beneficial_owners: list[dict[str, object]] | None = None + """For corporate UBOs: the owning entity's own beneficial owners, as raw objects extracted from the registry. Empty array when not applicable.""" + + effective_ownership_percent: float | None = None + """Parsed numeric ownership percentage - the max bound when available, otherwise the min bound. `null` when neither bound could be parsed.""" + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories(BaseModel): + """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" + + sanctions: Literal["match", "clear"] | str | None = None + + pep: Literal["match", "clear"] | str | None = None + + adverse_media: Literal["match", "clear"] | str | None = None + + warnings: Literal["match", "clear"] | str | None = None + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures( + BaseModel +): + feature: str | None = None + """Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`).""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status inside the child session.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + """Officer's full name as recorded in the registry.""" + + designation: str | None = None + """Free-text designation from the registry (e.g. `Director`, `Company Secretary`).""" + + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other", + ] + | str + | None + ) = None + """Normalized officer role.""" + + nationality: str | None = None + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + is_active: bool | None = None + + address: str | None = None + """Officer address reported by the registry.""" + + aml_status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Status of the AML screening run for this officer. `null` when no AML screening was run.""" + + kyc_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Raw lifecycle status of the child KYC session spawned for this officer. This is the full session status enum (not a reduced Approved/Declined/Pending set). `null` when no child KYC session exists.""" + + kyc_session_url: str | None = None + """Shareable URL of the child KYC session. `null` when no child session exists.""" + + kyc_session_id: str | None = None + """Session id of the child KYC session. Pass it to this same decision endpoint to fetch the person's full KYC report. `null` when no child session exists.""" + + verification_workflow_type: str | None = None + """Base workflow type of the child session's workflow (e.g. `kyc`). `null` for graph-based workflows or when no child session exists.""" + + verification_workflow_id: str | None = None + """Workflow id used by the child session. `null` when no child session exists.""" + + verification_workflow_label: str | None = None + """Human-readable label of the child session's workflow. `null` when unset.""" + + verification_workflow_features: ( + list[ + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersVerificationWorkflowFeatures + ] + | None + ) = None + """Per-feature progress of the child verification session, e.g. `[{"feature": "ID_VERIFICATION", "status": "Approved"}]`. `null` when the child session has no resolvable feature list.""" + + didit_internal_id: str | None = None + """Stable internal identifier of the verified individual behind the child session - useful for cross-session linking. `null` when no child session exists or the user has not been resolved yet.""" + + kyc_document_type: str | None = None + """Document type presented in the child KYC session (e.g. `P`, `ID`, `DL`). `null` until the child session has an ID verification result.""" + + kyc_nationality: str | None = None + """Nationality extracted from the document in the child KYC session. `null` until available.""" + + kyc_liveness_passed: bool | None = None + """Whether the child KYC session's liveness check passed. `null` when no liveness check has run.""" + + kyc_verified_at: _dt.datetime | None = None + """When the child KYC session reached `Approved`. `null` while the child session has any other status.""" + + kyc_aml_categories: ( + SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficersKycAmlCategories | None + ) = None + """Per-category summary derived from the person's AML hits. `null` when there are no hits.""" + + kyc_notification_sent_at: str | None = None + """ISO 8601 timestamp of the last verification-invite notification sent for this person's child session. `null` when none was sent.""" + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry(BaseModel): + """Parties discovered via the registry check.""" + + officers: list[SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryOfficers] | None = None + """Officers discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.""" + + beneficial_owners: ( + list[SessionsRetrieveDecisionResponseKeyPeopleChecksRegistryBeneficialOwners] | None + ) = None + """Beneficial owners discovered via the registry check, each enriched with the status of any child KYC session and AML screening spawned for them.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles(BaseModel): + role: ( + Literal[ + "director", + "non_executive_director", + "secretary", + "chairman", + "ubo", + "shareholder", + "representative", + "founder", + "legal_advisor", + "authorized_signatory", + "trustee", + "beneficiary", + "settlor", + "protector", + "company_officer", + "investor", + "other", + ] + | str + | None + ) = None + + ownership_percent: float | None = None + + voting_percent: float | None = None + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures( + BaseModel +): + feature: str | None = None + """Feature display name (e.g. `ID_VERIFICATION`, `LIVENESS`, `AML`).""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status inside the child session.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties(BaseModel): + """Party submitted by the business admin via the Key People flow (source=USER).""" + + uuid: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + + source: Literal["USER"] | str | None = None + """Always `USER` here - this bucket only contains parties entered or confirmed by the end user in the Key People flow (registry-discovered people appear under `registry`).""" + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + company_name: str | None = None + + registration_number: str | None = None + + email: str | None = None + + nationality: str | None = None + """Nationality for persons; country of incorporation for company parties.""" + + phone_number: str | None = None + + date_of_birth: _dt.date | None = None + + position: str | None = None + """Free-text job title / position (persons only).""" + + custom_fields: dict[str, object] | None = None + """Workflow-defined custom fields collected for this party; keys are defined by the workflow's key-people field descriptors.""" + + is_active: bool | None = None + + is_skipped: bool | None = None + """True when the end user skipped verification for this party (only possible when the role allows skipping).""" + + requires_verification: bool | None = None + """True when a child KYC / KYB sub-session is required for this party based on role settings and ownership thresholds.""" + + roles: list[SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesRoles] | None = None + """All roles held by this party, each with its own ownership and voting percentages. There is no scalar `role`/`ownership_percent` on the party itself.""" + + kyc_session_id: str | None = None + """Child KYC session id for person parties requiring verification. `null` otherwise.""" + + kyc_session_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Lifecycle status of the child KYC session. `null` when no child KYC session exists.""" + + kyc_session_url: str | None = None + + kyb_sub_session_id: str | None = None + """Child KYB sub-session id for corporate parties requiring verification. `null` otherwise.""" + + kyb_sub_session_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """Lifecycle status of the child KYB sub-session. `null` when no sub-session exists.""" + + kyb_sub_session_url: str | None = None + """Shareable URL of the child KYB sub-session. `null` when no sub-session exists.""" + + verification_workflow_type: str | None = None + """Base workflow type of the child session's workflow (KYC session preferred, falling back to the KYB sub-session). `null` for graph-based workflows or when no child session exists.""" + + verification_workflow_id: str | None = None + """Workflow id used by the child session. `null` when no child session exists.""" + + verification_workflow_label: str | None = None + """Human-readable label of the child session's workflow. `null` when unset.""" + + verification_workflow_features: ( + list[ + SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedPartiesVerificationWorkflowFeatures + ] + | None + ) = None + """Per-feature progress of the child verification session, e.g. `[{"feature": "ID_VERIFICATION", "status": "Approved"}]`. `null` when the child session has no resolvable feature list.""" + + didit_internal_id: str | None = None + """Stable internal identifier of the verified individual behind the child session. `null` when not resolved.""" + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted(BaseModel): + """Parties submitted by the business admin during the Key People flow (BusinessParty rows where source=USER).""" + + parties: list[SessionsRetrieveDecisionResponseKeyPeopleChecksSubmittedParties] | None = None + + +class SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary(BaseModel): + """Aggregate UBO KYC progress, or null when no UBOs have linked KYC sessions.""" + + total: int | None = None + + approved: int | None = None + + flagged: int | None = None + """UBOs whose KYC is Declined or In Review.""" + + pending: int | None = None + + +class SessionsRetrieveDecisionResponseKeyPeopleChecks(BaseModel): + """Aggregate check over the company's officers + UBOs. Uses the `registry` / `submitted` shape. Present only when session_kind = business.""" + + status: str | None = None + + node_id: str | None = None + + registry: SessionsRetrieveDecisionResponseKeyPeopleChecksRegistry | None = None + """Parties discovered via the registry check.""" + + submitted: SessionsRetrieveDecisionResponseKeyPeopleChecksSubmitted | None = None + """Parties submitted by the business admin during the Key People flow (BusinessParty rows where source=USER).""" + + ubo_kyc_summary: SessionsRetrieveDecisionResponseKeyPeopleChecksUboKycSummary | None = None + """Aggregate UBO KYC progress, or null when no UBOs have linked KYC sessions.""" + + warnings: list[dict[str, object]] | None = None + + +class SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails(BaseModel): + """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" + + full_name: str | None = None + + document_type: str | None = None + + document_number: str | None = None + + +class SessionsRetrieveDecisionResponseLivenessChecksMatches(BaseModel): + session_id: str | None = None + """Session ID of the matching face. `null` when `source` is `imported`, `list_entry`, or `retained_template`.""" + + session_number: int | None = None + + similarity_percentage: float | None = None + """Face similarity between the two images, clamped to 0–100.""" + + vendor_data: str | None = None + """`vendor_data` of the matching session or linked User for imported and retained-template faces.""" + + vendor_user_id: str | None = None + """Linked User id for `imported` and `retained_template` matches and for session matches linked to a User. `null` for list entries and sessions without a User.""" + + biometric_template_id: str | None = None + """Id of the retained biometric template. Set only when `source` is `retained_template`; `null` otherwise.""" + + verification_date: _dt.datetime | None = None + """When the matching face was captured, or when a retained template was created. `null` for list entries.""" + + user_details: SessionsRetrieveDecisionResponseLivenessChecksMatchesUserDetails | None = None + """Identity data from the matching session or imported User profile. Always `null` for `retained_template` and `list_entry` matches.""" + + match_image_url: str | None = None + """Reference image for image-backed matches. Always `null` for `retained_template` matches because retained templates contain no image.""" + + status: str | None = None + """Lifecycle status of the matching session. `null` for non-session sources.""" + + is_blocklisted: bool | None = None + """Whether the matching face is on a blocklist.""" + + is_allowlisted: bool | None = None + """Whether the matching face is on an allowlist.""" + + api_service: str | None = None + """Standalone API service of the matching session (e.g. `PASSIVE_LIVENESS`, `FACE_MATCH`); `null` for workflow sessions and non-session sources.""" + + source: Literal["session", "imported", "list_entry", "retained_template"] | str | None = None + """Origin of the matching face: `session`, `imported` (User face upload), `list_entry` (allow/block list), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`).""" + + +class SessionsRetrieveDecisionResponseLivenessChecksWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseLivenessChecks(BaseModel): + """One liveness report - the full report for a single liveness pass.""" + + node_id: str | None = None + """Workflow graph node id of the liveness step that produced this report.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this liveness step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + method: str | None = None + """Method used for liveness detection""" + + score: float | None = None + """Confidence score of the liveness check""" + + reference_image: str | None = None + """URL of the reference image used""" + + video_url: str | None = None + """URL of the liveness check video""" + + age_estimation: float | None = None + """Estimated age of the person""" + + matches: list[SessionsRetrieveDecisionResponseLivenessChecksMatches] | None = None + """Similar faces found by the duplicate-face search (top matches, blocklisted/allowlisted entries first). Each entry's `source` tells you where the matching face came from: `session` (another verification session), `imported` (a face uploaded against a vendor user), `list_entry` (an org-managed allow/block list face), or `retained_template` (an image-free biometric template stored on a User, listed under `/v3/biometric-templates/`). `session_id`, `session_number`, `status` and `api_service` are `null` for non-session sources. `imported` matches still populate `vendor_data`, `verification_date` and - when the vendor user has a name on file - `user_details` (with `full_name` only) from the vendor-user record; `retained_template` matches populate `vendor_user_id`, `vendor_data`, `biometric_template_id` and `verification_date`, while their image and identity fields are `null`; `list_entry` matches return all of these as `null`.""" + + warnings: list[SessionsRetrieveDecisionResponseLivenessChecksWarnings] | None = None + + face_quality: float | None = None + """Face image quality score (0-100%). Only available for passive liveness.""" + + face_luminance: float | None = None + """Face luminance/brightness score (0-100%). Only available for passive liveness.""" + + +class SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity(BaseModel): + sod_integrity: bool | None = None + + dg_integrity: bool | None = None + + +class SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary(BaseModel): + issuer: str | None = None + + subject: str | None = None + + serial_number: str | None = None + + not_valid_after: _dt.datetime | None = None + + not_valid_before: _dt.datetime | None = None + + +class SessionsRetrieveDecisionResponseNfcVerificationsChipData(BaseModel): + document_type: str | None = None + + issuing_country: str | None = None + + document_number: str | None = None + + expiration_date: _dt.date | None = None + + first_name: str | None = None + + last_name: str | None = None + + birth_date: _dt.date | None = None + + gender: str | None = None + + nationality: str | None = None + + address: str | None = None + + place_of_birth: str | None = None + + +class SessionsRetrieveDecisionResponseNfcVerificationsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponseNfcVerifications(BaseModel): + """One NFC report - the full report for a single ePassport chip read.""" + + node_id: str | None = None + """Workflow graph node id of the NFC step that produced this report. Use it to correlate the report with the workflow definition.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this NFC step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + is_nfc_skipped: bool | None = None + """`true` when this NFC step was explicitly skipped instead of read; `skip_reason` says why.""" + + skip_reason: ( + Literal[ + "USER_SKIPPED", + "DOCUMENT_WITHOUT_CHIP", + "CHIP_CERTIFICATE_UNAVAILABLE", + "DEVICE_WITHOUT_NFC", + "INTEGRATION_WITHOUT_NFC_ACCESS", + "MRZ_KEY_UNAVAILABLE", + ] + | str + | None + ) = None + """Why this NFC step was skipped. Only set when `is_nfc_skipped` is `true`. Same taxonomy as the session-level `nfc_skip_reason`, which additionally covers the cases where NFC never produced a report at all.""" + + portrait_image: str | None = None + + signature_image: str | None = None + + chip_data: SessionsRetrieveDecisionResponseNfcVerificationsChipData | None = None + + authenticity: SessionsRetrieveDecisionResponseNfcVerificationsAuthenticity | None = None + + certificate_summary: ( + SessionsRetrieveDecisionResponseNfcVerificationsCertificateSummary | None + ) = None + + warnings: list[SessionsRetrieveDecisionResponseNfcVerificationsWarnings] | None = None + + +class SessionsRetrieveDecisionResponsePhoneVerificationsCarrier(BaseModel): + name: str | None = None + """Name of the phone carrier""" + + type: str | None = None + """Type of phone line (mobile, landline, etc)""" + + +class SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails(BaseModel): + """Event-specific payload. Send events carry `status`, `reason`, `channel`, `actual_channel`; delivery events carry `channel`, `status`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `PHONE_VERIFICATION_APPROVED`).""" + + status: str | None = None + + reason: str | None = None + + channel: str | None = None + + actual_channel: str | None = None + + code_tried: str | None = None + + +class SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle(BaseModel): + type: str | None = None + + timestamp: _dt.datetime | None = None + + details: SessionsRetrieveDecisionResponsePhoneVerificationsLifecycleDetails | None = None + """Event-specific payload. Send events carry `status`, `reason`, `channel`, `actual_channel`; delivery events carry `channel`, `status`; code-check events carry `code_tried`, `status`; final status events carry `reason` - or `null` when there is no reason to report (e.g. `PHONE_VERIFICATION_APPROVED`).""" + + fee: float | None = None + + +class SessionsRetrieveDecisionResponsePhoneVerificationsMatches(BaseModel): + session_id: str | None = None + + session_number: int | None = None + + vendor_data: str | None = None + + verification_date: _dt.datetime | None = None + + phone_number: str | None = None + + status: str | None = None + + is_blocklisted: bool | None = None + + api_service: str | None = None + + source: Literal["session", "list_entry"] | str | None = None + """Where this match came from. `session` means another verification session in your application matched on this value. `list_entry` means the value was hit against an org-managed list (allowlist/blocklist) - see the management API lists overview at `/management-api/lists/overview` for how to manage those lists.""" + + +class SessionsRetrieveDecisionResponsePhoneVerificationsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponsePhoneVerifications(BaseModel): + node_id: str | None = None + """Unique identifier for this phone step in the workflow graph.""" + + status: Literal["Not Finished", "Approved", "Declined", "In Review", "Expired"] | str | None = ( + None + ) + """Feature-level status of this phone verification step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + phone_number_prefix: str | None = None + """Country calling code prefix""" + + phone_number: str | None = None + """Phone number without country prefix""" + + full_number: str | None = None + """Complete phone number with country prefix""" + + country_code: str | None = None + """Two-letter country code (ISO 3166-1 alpha-2)""" + + country_name: str | None = None + """Full country name""" + + carrier: SessionsRetrieveDecisionResponsePhoneVerificationsCarrier | None = None + + is_disposable: bool | None = None + """Whether the phone number is from a disposable service""" + + is_virtual: bool | None = None + """Whether the phone number is virtual""" + + verification_method: str | None = None + """Method used to verify the phone number""" + + verification_attempts: int | None = None + """Number of verification attempts made""" + + verified_at: _dt.datetime | None = None + """Timestamp when the phone was verified""" + + lifecycle: list[SessionsRetrieveDecisionResponsePhoneVerificationsLifecycle] | None = None + """Chronological list of events in the phone verification lifecycle.""" + + warnings: list[SessionsRetrieveDecisionResponsePhoneVerificationsWarnings] | None = None + """List of warnings related to phone verification""" + + matches: list[SessionsRetrieveDecisionResponsePhoneVerificationsMatches] | None = None + """Matches found for this phone number. Each match has a `source`: `session` (another verification session in your application matched on the same number, returned with `session_id`/`session_number`/`vendor_data`/`verification_date`) or `list_entry` (the number is on an org-managed allow/block list - see `/management-api/lists/overview`). Cross-session matches use different `vendor_data` and are capped at 5.""" + + +class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions( + BaseModel +): + page: int | None = None + + x: float | None = None + + y: float | None = None + + width: float | None = None + + height: float | None = None + + page_width: float | None = None + + page_height: float | None = None + + +class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation( + BaseModel +): + """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" + + detected: bool | None = None + """Whether overlay-text manipulation evidence was detected.""" + + analyzed: bool | None = None + """Whether the PDF could be analyzed.""" + + signals: list[str] | None = None + """Forensic signals that fired, such as duplicate_font_subset or glyph_fragmentation.""" + + manipulated_regions: ( + list[ + SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulationManipulatedRegions + ] + | None + ) = None + """Suspected manipulated areas in PDF page coordinates.""" + + +class SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata(BaseModel): + """File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted.""" + + file_size: int | None = None + """Size of the document""" + + content_type: str | None = None + """Content type of the document""" + + creation_date: _dt.date | None = None + """Date when the document was created""" + + modified_date: _dt.date | None = None + """Date when the document was modified""" + + overlay_manipulation: ( + SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadataOverlayManipulation | None + ) = None + """PDF forensic analysis for suspected overlay-text manipulation. Null when no overlay manipulation evidence was found or the document was not a PDF.""" + + creator: str | None = None + """Creator application recorded in the document metadata.""" + + producer: str | None = None + """Producer software recorded in the PDF metadata.""" + + software: str | None = None + """Editing/processing software recorded in the file metadata.""" + + encryption: str | None = None + """Encryption scheme of the document, when encrypted.""" + + is_signed: bool | None = None + """Whether the PDF carries a digital signature.""" + + is_tampered: bool | None = None + """Whether signature validation indicates the file was modified after signing.""" + + signature_info: dict[str, object] | None = None + """Details of the digital signature(s), when present.""" + + exif_original_date: str | None = None + """EXIF original capture date, for image uploads.""" + + exif_digitized_date: str | None = None + """EXIF digitization date, for image uploads.""" + + processed_by_known_editor: bool | None = None + """Whether the file metadata indicates processing by a known document editor.""" + + has_different_creation_mod_date: bool | None = None + """Whether the creation and modification dates differ - a common manipulation signal.""" + + +class SessionsRetrieveDecisionResponsePoaVerificationsExtraFields(BaseModel): + """Additional banking and contact details extracted from the document.""" + + bank_account_number: str | None = None + + bank_iban: str | None = None + + bank_sort_code: str | None = None + + bank_routing_number: str | None = None + + bank_swift_bic: str | None = None + + bank_branch_name: str | None = None + + bank_branch_address: str | None = None + + document_phone_number: str | None = None + + additional_names: list[str] | None = None + + +class SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation(BaseModel): + latitude: float | None = None + """Latitude coordinate""" + + longitude: float | None = None + """Longitude coordinate""" + + +class SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress(BaseModel): + address_type: str | None = None + """Type of address (Street, Avenue, etc)""" + + street_1: str | None = None + """Primary street address""" + + street_2: str | None = None + """Secondary street address""" + + city: str | None = None + """City name""" + + region: str | None = None + """Region or state name""" + + country: str | None = None + """Two-letter country code""" + + postal_code: str | None = None + """Postal code""" + + document_location: ( + SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddressDocumentLocation | None + ) = None + + +class SessionsRetrieveDecisionResponsePoaVerificationsWarnings(BaseModel): + feature: ( + Literal[ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACEMATCH", + "PROOF_OF_ADDRESS", + "PHONE", + "EMAIL", + "AML", + "LOCATION", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_REGISTRY", + "KYB_DOCUMENTS", + "KYB_KEY_PEOPLE", + ] + | str + | None + ) = None + """The feature that generated this warning. The value identifies the feature block the warning belongs to. Each module's warnings array typically uses the value matching its module (e.g. `PHONE` inside `phone_verifications[].warnings`), but the enum above is authoritative across all feature warnings on the decision payload.""" + + risk: str | None = None + + additional_data: dict[str, object] | None = None + + log_type: str | None = None + + short_description: str | None = None + + long_description: str | None = None + + node_id: str | None = None + """The workflow graph node ID that generated this warning""" + + +class SessionsRetrieveDecisionResponsePoaVerifications(BaseModel): + node_id: str | None = None + """Unique identifier for this POA step in the workflow graph.""" + + status: ( + Literal["Not Finished", "Approved", "Declined", "In Review", "Resub Requested"] | str | None + ) = None + """Feature-level status of this Proof of Address step. Independent of the top-level session `status` - a session can reach its overall decision while an individual feature stays `In Review`.""" + + document_file: str | None = None + """URL of the proof of address document""" + + issuing_state: str | None = None + """Two-letter country code of the issuing state""" + + document_type: str | None = None + """Type of proof of address document""" + + document_subtype: str | None = None + """More specific classification of the POA document within its `document_type`, when one was detected. `null` otherwise.""" + + document_language: str | None = None + """Language code of the document""" + + document_metadata: SessionsRetrieveDecisionResponsePoaVerificationsDocumentMetadata | None = ( + None + ) + """File-forensics metadata extracted from the uploaded POA document. Always contains the keys below (values are `null` when not applicable, e.g. EXIF dates for PDFs). `null` when no metadata could be extracted.""" + + issuer: str | None = None + """Name of the document issuer""" + + issue_date: _dt.date | None = None + """Date when the document was issued""" + + expiration_date: _dt.date | None = None + """Expiration date of the document, if available""" + + poa_address: str | None = None + """Raw address from the proof of address document""" + + poa_formatted_address: str | None = None + """Formatted address from the proof of address document""" + + poa_parsed_address: SessionsRetrieveDecisionResponsePoaVerificationsPoaParsedAddress | None = ( + None + ) + + name_on_document: str | None = None + """Account-holder / addressee name extracted from the POA document, used for name matching. `null` when no name could be extracted.""" + + name_match_score_expected_details: float | None = None + """Similarity score (0-100) between `name_on_document` and the expected name supplied at session creation. `null` when either side is missing.""" + + name_match_score_id_verification: float | None = None + """Similarity score (0-100) between `name_on_document` and the name extracted by the ID verification step. `null` when either side is missing.""" + + expected_details_address: str | None = None + """Expected raw address for verification""" + + expected_details_formatted_address: str | None = None + """Expected formatted address for verification""" + + expected_details_parsed_address: dict[str, object] | None = None + """Expected parsed address details for verification""" + + extra_fields: SessionsRetrieveDecisionResponsePoaVerificationsExtraFields | None = None + """Additional banking and contact details extracted from the document.""" + + extra_files: list[str] | None = None + """Additional proof of address files""" + + warnings: list[SessionsRetrieveDecisionResponsePoaVerificationsWarnings] | None = None + + detected_codes: list[DocumentAIDetectedCode] | None = None + """QR codes and barcodes detected across every processed page of the document. Empty when none are found. A located but undecodable QR code has `parsed_payload: null`.""" + + +class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer(BaseModel): + """Answer provided for the item""" + + value: str | None = None + + text: str | None = None + + files: list[str] | None = None + + +class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices(BaseModel): + label: str + + value: str + + requires_text_input: bool | None = None + """Whether this choice requires additional text input""" + + +class SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems(BaseModel): + uuid: str + """Unique identifier for the item""" + + value: str | None + """Stable node identifier of this form element within the questionnaire graph.""" + + element_type: str + """Type of the questionnaire element""" + + is_required: bool + """Whether the item is required""" + + title: str + """Title of the item""" + + description: str | None = None + """Description of the item""" + + placeholder: str | None = None + """Placeholder text""" + + choices: ( + list[SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsChoices] | None + ) = None + """Choices for dropdown or single choice elements""" + + max_files: int | None = None + """Maximum number of files allowed""" + + answer: SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItemsAnswer | None = None + """Answer provided for the item""" + + required_if: dict[str, object] | None = None + """Conditional-requirement rule, when the element is only required for certain answers.""" + + repeatable_config: dict[str, object] | None = None + """Configuration for repeatable element groups, when applicable.""" + + +class SessionsRetrieveDecisionResponseQuestionnaireResponsesSections(BaseModel): + title: str | None = None + """Section title""" + + description: str | None = None + """Section description""" + + items: list[SessionsRetrieveDecisionResponseQuestionnaireResponsesSectionsItems] | None = None + + +class SessionsRetrieveDecisionResponseQuestionnaireResponses(BaseModel): + node_id: str | None = None + """Unique identifier for this questionnaire step in the workflow graph.""" + + questionnaire_id: str | None = None + """Unique identifier for the questionnaire""" + + title: str | None = None + """Title of the questionnaire""" + + description: str | None = None + """Description of the questionnaire""" + + languages: list[str] | None = None + """Supported languages""" + + default_language: str | None = None + """Default language""" + + is_active: bool | None = None + """Whether the questionnaire is active""" + + is_simple_questionnaire: bool | None = None + """Whether the questionnaire is a simple linear form (no branching graph).""" + + questionnaire_group_id: str | None = None + """Stable identifier of the questionnaire group across versions.""" + + version: int | None = None + """Version number of the questionnaire that was answered.""" + + published_at: _dt.datetime | None = None + """Timestamp at which this questionnaire version was published.""" + + sections: list[SessionsRetrieveDecisionResponseQuestionnaireResponsesSections] | None = None + + status: str | None = None + """Status of the questionnaire""" + + +class SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners(BaseModel): + uuid: str | None = None + + name: str | None = None + + first_name: str | None = None + + last_name: str | None = None + + entity_type: Literal["person", "company"] | str | None = None + + roles: list[str] | None = None + + ownership_min_shares: str | None = None + """Lower bound of the ownership percentage as reported by the registry. Free-form string (e.g. `"25.00"`, `"50"`); use `effective_ownership_percent` for a parsed numeric value.""" + + ownership_max_shares: str | None = None + """Upper bound of the ownership percentage as reported by the registry. Free-form string; use `effective_ownership_percent` for a parsed numeric value.""" + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + effective_ownership_percent: float | None = None + + company_name: str | None = None + + registration_number: str | None = None + + country_of_birth: str | None = None + + email: str | None = None + + nationality: str | None = None + + designation: str | None = None + + voting_min_shares: str | None = None + + voting_max_shares: str | None = None + + date_of_birth: _dt.date | None = None + """Complete birth date when reported.""" + + appointment_date: _dt.date | None = None + """Appointment date when reported.""" + + shareholding: KYBRegistryShareholding | None = None + """What an owner holds, in the same shape for every country and registry source. Null when the registry states no holding. A band (for example a UK PSC's 25-50%) is not a stake: `percentage` is null and the band stays in `ownership_min_shares` / `ownership_max_shares`.""" + + +class SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers(BaseModel): + uuid: str | None = None + + name: str | None = None + + designation: str | None = None + + role: str | None = None + + nationality: str | None = None + + is_active: bool | None = None + + kyc_status: Literal["Approved", "Declined", "Pending"] | str | None = None + + kyc_session_url: str | None = None + + first_name: str | None = None + """Given name when explicitly reported by the registry; not inferred from the full name.""" + + last_name: str | None = None + """Family name when explicitly reported by the registry.""" + + address: str | None = None + """Officer address reported by the registry.""" + + appointment_date: _dt.date | None = None + """Date the officer was appointed.""" + + termination_date: _dt.date | None = None + """Date the appointment ended, when reported.""" + + date_of_birth: _dt.date | None = None + """Complete registry-reported birth date. Null when only a birth year or month is known.""" + + date_of_birth_year: int | None = None + """Registry-reported birth year, including when the full date is unavailable.""" + + date_of_birth_month: int | None = None + """Registry-reported birth month. Null when only the year is known.""" + + +class SessionsRetrieveDecisionResponseRegistryChecksCompany(BaseModel): + """Full registry payload for the company (registry record merged with any user-provided or console-edited data).""" + + uuid: str | None = None + + node_id: str | None = None + + status: str | None = None + + registry_status: str | None = None + + data_resolved: bool | None = None + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """Country of incorporation (ISO 3166-1 alpha-2, e.g. `GB`, `DE`, `US`).""" + + region: str | None = None + """ISO 3166-2 subdivision code when applicable (e.g. `CA`, `NY` for US states).""" + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + + tax_number: str | None = None + + risk_level: Literal["LOW", "MEDIUM", "HIGH"] | str | None = None + + verification_status: str | None = None + + is_from_registry: bool | None = None + + fetch_status: str | None = None + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Registered/share capital amount as a decimal string (e.g. `"100.00"`), parsed from the registry filing. `null` when not available.""" + + registered_capital_currency: str | None = None + """ISO 4217 currency code of `registered_capital_amount` (e.g. `"GBP"`). `null` when not available.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + + location_of_registration: str | None = None + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + + officers: list[SessionsRetrieveDecisionResponseRegistryChecksCompanyOfficers] | None = None + + beneficial_owners: ( + list[SessionsRetrieveDecisionResponseRegistryChecksCompanyBeneficialOwners] | None + ) = None + + addresses: list[KYBRegistryAddress] | None = None + """Every address the registry states, one canonical shape. Null when it states none.""" + + industries: list[KYBRegistryIndustry] | None = None + """The activities the registry classifies the company under, one canonical shape. Null when it states none.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw record returned by the registry source, kept for reviewers. Its shape differs by country and source and is not a stable contract: read the canonical fields (`addresses`, `industries`, `ownership`, `beneficial_owners[].shareholding`) instead.""" + + user_provided_data: dict[str, object] | None = None + """Data confirmed or edited by the end user.""" + + confirmed_by_user_at: _dt.datetime | None = None + + last_console_edit_at: _dt.datetime | None = None + + is_editable: bool | None = None + + ownership: KYBRegistryOwnership | None = None + """The outcome of the ownership (Shareholders / UBO) tier, in the same shape for every country and registry source.""" + + registry_tier: str | None = None + """The registry tier the workflow ordered: `basic`, `shareholders` or `ubo`. Also in `ownership.tier_requested`.""" + + registry_tier_delivered: str | None = None + """The tier the registry delivered and that is billed. Also in `ownership.tier_delivered`.""" + + registry_ownership_status: str | None = None + """`not_requested`, `pending`, `delivered` or `unavailable`. Also in `ownership.status`.""" + + +class SessionsRetrieveDecisionResponseRegistryChecks(BaseModel): + """One company registry check - the full company-registry payload enriched with ownership structure and warnings. Present only when `session_kind = business`.""" + + status: str | None = None + """Feature lifecycle status.""" + + node_id: str | None = None + + data_resolved: bool | None = None + + company: SessionsRetrieveDecisionResponseRegistryChecksCompany | None = None + """Full registry payload for the company (registry record merged with any user-provided or console-edited data).""" + + ownership_structure: dict[str, object] | None = None + """Raw ownership snapshot kept for reviewers; its shape differs by registry source and is not a stable contract. Read `company.beneficial_owners` and `company.ownership` instead.""" + + warnings: list[dict[str, object]] | None = None + + +class SessionsRetrieveDecisionResponseResubmissionsNodes(BaseModel): + node_id: str | None = None + + feature: str | None = None + + +class SessionsRetrieveDecisionResponseResubmissionsRequestedBy(BaseModel): + """Who requested the resubmission.""" + + name: str | None = None + + email: str | None = None + + type: str | None = None + + +class SessionsRetrieveDecisionResponseResubmissions(BaseModel): + number: int | None = None + """1 for the first resubmission of the session, 2 for the second, and so on.""" + + created_at: _dt.datetime | None = None + """When the resubmission was requested.""" + + previous_status: str | None = None + """The session status just before the request.""" + + nodes: list[SessionsRetrieveDecisionResponseResubmissionsNodes] | None = None + """The workflow steps sent back to the user.""" + + reasons: dict[str, str] | None = None + """The reason given for each step, keyed by `node_id`.""" + + requested_by: SessionsRetrieveDecisionResponseResubmissionsRequestedBy | None = None + """Who requested the resubmission.""" + + results: dict[str, object] | None = None + """What the resubmitted steps returned before the request, under the same keys and in the same shape as the current results of this response (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `phone_verifications`, `email_verifications`, `aml_screenings`, `ip_analyses`, `database_validations`, `questionnaire_responses`, `document_ai_documents`, `geolocation_checks`; for business sessions also `document_verifications`). Only the keys of the steps that were sent back appear.""" + + +class SessionsRetrieveDecisionResponseReviews(BaseModel): + user: str | None = None + """Who performed the review. Usually the console reviewer's identifier (their email), but the value is `"API Client"` for status updates made with an API key and `"Unknown"` when no actor could be resolved - so do not parse this field as an email address.""" + + new_status: str | None = None + + comment: str | None = None + + created_at: _dt.datetime | None = None + + +class SessionsRetrieveDecisionResponse(BaseModel): + session_id: str | None = None + """The unique identifier of the session for which to retrieve verification results.""" + + session_kind: Literal["user", "business"] | str | None = None + """Discriminator indicating whether this is a User Verification (KYC) or Business Verification (KYB) session. When `user`, the KYC feature arrays (`id_verifications`, `nfc_verifications`, `liveness_checks`, `face_matches`, `poa_verifications`, `document_ai_documents`, `database_validations`, ...) apply and the business arrays are absent. When `business`, the payload instead carries `registry_checks`, `document_verifications`, `key_people_checks` plus the shared arrays (`aml_screenings`, `phone_verifications`, `email_verifications`, `ip_analyses`, `questionnaire_responses`). **Omitted when `include=events` is passed** - the events shape only exists for user sessions, so the discriminator is dropped from that variant.""" + + session_number: int | None = None + """The number of the session.""" + + session_url: str | None = None + """The URL of the session.""" + + status: ( + Literal[ + "Not Started", + "In Progress", + "Awaiting User", + "In Review", + "Approved", + "Declined", + "Resubmitted", + "Expired", + "Kyc Expired", + "Abandoned", + ] + | str + | None + ) = None + """Overall lifecycle status of the session - the rolled-up workflow result. Terminal values are `Approved`, `Declined`, `In Review`, `Expired`, `Kyc Expired`, and `Abandoned`; `Not Started`, `In Progress`, `Awaiting User`, and `Resubmitted` indicate the session is still moving through the workflow.""" + + workflow_id: str | None = None + """Stable identifier of the workflow group that produced this session. Workflows are versioned - this value identifies the workflow itself, not the specific version.""" + + features: list[str] | None = None + """List of verification features configured for this session, as display names: `ID_VERIFICATION`, `NFC`, `LIVENESS`, `FACE_MATCH`, `POA`, `PHONE`, `EMAIL_VERIFICATION`, `AML`, `IP_ANALYSIS`, `DATABASE_VALIDATION`, `QUESTIONNAIRE`, `AGE_ESTIMATION`, `FACE_SEARCH` (Business Verification sessions use the KYB feature set: `KYB_REGISTRY`, `KYB_DOCUMENTS`, `KYB_KEY_PEOPLE`, `AML`, `PHONE`, `EMAIL_VERIFICATION`, `IP_ANALYSIS`, `QUESTIONNAIRE`). With `include=events` each entry becomes an object `{feature, node_id}` instead of a plain string, and the email feature is renamed: it appears as `EMAIL` in events mode instead of the `EMAIL_VERIFICATION` used here.""" + + vendor_data: str | None = None + """The vendor data of the session.""" + + metadata: dict[str, object] | None = None + """The metadata of the session.""" + + expected_details: SessionsRetrieveDecisionResponseExpectedDetails | None = None + """Expected details supplied at session creation - used for cross-validation against extracted data (KYC) or to pre-fill the company registry search (KYB). Echoes back exactly the keys that were provided; `null` when none were supplied. KYC sessions carry the person fields; Business (KYB) sessions carry `company_name`, `registry_country` and `registration_number`.""" + + contact_details: SessionsRetrieveDecisionResponseContactDetails | None = None + """Contact details supplied when the session was created (used for notification emails, prefilled phone/email verification, and communication language). `null` when neither an email nor a phone number was provided.""" + + callback: str | None = None + """The callback URL of the session.""" + + id_verifications: list[SessionsRetrieveDecisionResponseIdVerifications] | None = None + """**Array of ID verification (OCR) reports** produced by ID-document steps in the workflow graph. Always a JSON array - read `id_verifications[0]` and iterate if your workflow can run more than one ID check (for example a primary check plus a step-up). `null` until at least one ID step has produced data. Each item is the full report, including `node_id`, feature-level `status`, extracted document fields, image URLs, quality scores, MRZ, address parsing, cross-session matches, and `warnings[]`.""" + + nfc_skip_reason: ( + Literal[ + "USER_SKIPPED", + "DOCUMENT_WITHOUT_CHIP", + "CHIP_CERTIFICATE_UNAVAILABLE", + "DEVICE_WITHOUT_NFC", + "INTEGRATION_WITHOUT_NFC_ACCESS", + "MRZ_KEY_UNAVAILABLE", + ] + | str + | None + ) = None + """Why NFC chip verification did not run, populated whenever the workflow includes an NFC step. `null` while NFC is pending or when the chip was actually read. Values: `USER_SKIPPED` - the user chose to skip on an NFC-capable device (allowed by `allow_nfc_skip`); `DOCUMENT_WITHOUT_CHIP` - the document has no readable NFC chip (not an ICAO 9303 electronic document); `CHIP_CERTIFICATE_UNAVAILABLE` - the issuing country's signing certificate (CSCA) covering the document's issue period is unavailable, so the chip signature cannot be verified; `DEVICE_WITHOUT_NFC` - the device reported no NFC reader; `INTEGRATION_WITHOUT_NFC_ACCESS` - the verification ran without a native SDK (e.g. in a web browser), which has no access to the phone's NFC reader; `MRZ_KEY_UNAVAILABLE` - the chip access key could not be derived from the document's MRZ.""" + + nfc_verifications: list[SessionsRetrieveDecisionResponseNfcVerifications] | None = None + """**Array of NFC / ePassport reports** produced by NFC steps in the workflow graph. **This is always a JSON array, never a singular `nfc` object** - read `nfc_verifications[0]` and iterate if your workflow can run NFC more than once. `null` until at least one NFC step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `chip_data`, `authenticity` (SOD + DG integrity), `certificate_summary`, and `warnings[]`. When the NFC step was bypassed before it could ever be offered (chipless document, missing certificate, no NFC capability reported, unreadable MRZ key), this stays `null` and `nfc_skip_reason` explains why; a skip at the step itself (user tapped skip, or the reader turned out unavailable at scan time) still produces a report with `is_nfc_skipped: true`.""" + + liveness_checks: list[SessionsRetrieveDecisionResponseLivenessChecks] | None = None + """**Array of liveness reports** produced by liveness steps in the workflow graph. Always a JSON array - never a singular `liveness` object. `null` until at least one liveness step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `method`, `score`, `reference_image`, `video_url`, optional `age_estimation`, cross-session `matches[]`, and `warnings[]`.""" + + face_matches: list[SessionsRetrieveDecisionResponseFaceMatches] | None = None + """**Array of face-match reports** produced by face-match steps in the workflow graph. Always a JSON array - never a singular `face_match` object. `null` until at least one face-match step has produced data. Each item is the full report, including `node_id`, feature-level `status`, similarity `score`, `source_image`, `target_image`, and `warnings[]`.""" + + phone_verifications: list[SessionsRetrieveDecisionResponsePhoneVerifications] | None = None + """**Array of phone verification reports** - always a JSON array, never a singular `phone` object. `null` until at least one phone step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the phone number and country metadata, `lifecycle[]`, `matches[]`, and `warnings[]`.""" + + email_verifications: list[SessionsRetrieveDecisionResponseEmailVerifications] | None = None + """**Array of email verification reports** - always a JSON array, never a singular `email` object. `null` until at least one email step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the email address, breach data, `lifecycle[]`, `matches[]`, and `warnings[]`.""" + + poa_verifications: list[SessionsRetrieveDecisionResponsePoaVerifications] | None = None + """**Array of Proof of Address (POA) reports** - always a JSON array, never a singular `poa` object. `null` until at least one POA step has produced data. Each item is the full report, including `node_id`, feature-level `status`, the document file URL, issuer, dates, parsed address, and `warnings[]`.""" + + document_ai_documents: list[SessionsRetrieveDecisionResponseDocumentAiDocuments] | None = None + """**Document AI reports grouped by workflow node** - always a JSON array, never a singular object. `null` until at least one Document AI step has produced data. Present only when `session_kind = user`. Each item is a node group with an aggregate `status`, the `node_id`, an `items[]` array of the per-document results (extracted data keyed by the configured field key), and `warnings`.""" + + questionnaire_responses: list[SessionsRetrieveDecisionResponseQuestionnaireResponses] | None = ( + None + ) + """**Array of questionnaire response reports** - always a JSON array, never a singular `questionnaire` object. `null` until at least one questionnaire step has produced data. Each item is the full report, including `node_id`, the questionnaire metadata, sections with form elements, and the user's answers.""" + + aml_screenings: list[SessionsRetrieveDecisionResponseAmlScreenings] | None = None + """**Array of AML screening reports** - always a JSON array, never a singular `aml` object. `null` until at least one AML step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `total_hits`, `entity_type`, `hits[]`, `score`, `screened_data`, `is_ongoing_monitoring_enabled`, and `warnings[]`.""" + + ip_analyses: list[SessionsRetrieveDecisionResponseIpAnalyses] | None = None + """**Array of Device & IP Analysis reports** - always a JSON array, never a singular `ip_analysis` object. `null` until at least one IP-analysis step has produced data. Each item is the full report, including `node_id`, feature-level `status`, IP and geolocation data, device fingerprint, and `warnings[]`. Entries with duplicate `(node_id, ip_address, device_fingerprint)` are deduplicated server-side.""" + + database_validations: list[SessionsRetrieveDecisionResponseDatabaseValidations] | None = None + """**Array of Database Validation reports** - always a JSON array, never a singular `database_validation` object. `null` until at least one DB validation step has produced data. Each item is the full report, including `node_id`, feature-level `status`, `screened_data`, the per-service `validations[]` (each with its registry `source_data`), and `warnings[]`. The `screened_data` and `source_data` fields vary by country: + + **ARG (Argentina)**: screened_data: {document_number, selfie, gender, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, date_of_death, tax_id, tax_id_type, face_match_score, sit_1_since, banks, last_position, highest_position, rejected_checks}. Uses RENAPER biometric face-match validation. Gender is auto-inferred from face analysis or given name when not present in the document (e.g., driver licenses) + + **BOL (Bolivia)**: screened_data: {document_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, gender} + + **BRA (Brazil)**: screened_data: {tax_number, first_name, last_name, date_of_birth}. source_data always includes {identification_number, first_name, last_name, date_of_birth, lgpd_minor, minor_under_18, minor_under_16} for successful CPF matches. Successful adult lookups set those flags to false. The Receita Federal Consulta CPF service may return HTTP 422 (minor under 18) or 451 (under 16) with LGPD-withheld data; in those cases source_data sets the minor flags to true as appropriate, includes the upstream HTTP status, and field-level validation is no_match. See the Brazil section of the Database Validation Outcome Codes reference for the full code list. + + **CHL (Chile)**: screened_data: {personal_number, first_name, last_name, date_of_birth}. source_data: {identification_number, first_name, last_name, date_of_birth, gender} + + **COL (Colombia)**: screened_data: {document_number, document_type, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, document_type} + + **CRI (Costa Rica)**: screened_data: {personal_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name} + + **DOM (Dominican Republic)**: screened_data: {personal_number}. source_data: {identification_number} (returns valid/invalid only) + + **ECU (Ecuador)**: screened_data: {personal_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, street, formatted_address, gender, nationality, education, marital_status, spouse, mother_name, father_name, profession} + + **ESP (Spain)**: screened_data: {personal_number, document_type, expiration_date, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, document_type, expiration_date} + + **GTM (Guatemala)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name} + + **HND (Honduras)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name} + + **MEX (Mexico)**: screened_data: {personal_number (CURP), first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, date_of_birth, gender, nationality, curp_status, state_of_birth, doc_probatorio, registration_year, registration_state, registration_municipality, num_acta, crip, issuing_state_code, foreign_registry_number, folio_carta, folio_certificado}. Uses the RENAPO civil registry. A separate INE validity-check service is also available for MEX as an additional one-by-one validation. + + **PAN (Panama)**: screened_data: {personal_number, selfie}. source_data: {identification_number}. Uses SIB biometric validation + + **PER (Peru)**: screened_data: {personal_number (DNI), first_name, last_name}. source_data: {identification_number, first_name, last_name, paternal_name, maternal_name, verification_number, verification_letter} + + **PRY (Paraguay)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name} + + **SLV (El Salvador)**: screened_data: {document_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name} + + **URY (Uruguay)**: screened_data: {personal_number, date_of_birth, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name} + + **VEN (Venezuela)**: screened_data: {document_number, first_name, last_name}. source_data: {identification_number, first_name, last_name, full_name, gender, date_of_birth, document_type} + """ + + reviews: list[SessionsRetrieveDecisionResponseReviews] | None = None + + created_at: _dt.datetime | None = None + """Timestamp at which the session was created (ISO 8601, UTC).""" + + expires_at: _dt.datetime | None = None + """Timestamp at which this session is scheduled to expire. Set on User Verification (KYC) sessions when an expiration was configured at creation time (or once `Approved` status triggers KYC expiry computation). `null` for sessions without a configured expiration, and for kinds that do not use expiry (e.g. Business Verification (KYB) sessions that never received an `expires_at`).""" + + registry_checks: list[SessionsRetrieveDecisionResponseRegistryChecks] | None = None + """Company registry checks. Present only when `session_kind = business`. Each item wraps the full company-registry payload.""" + + key_people_checks: list[SessionsRetrieveDecisionResponseKeyPeopleChecks] | None = None + """Aggregate key-people checks. Present only when `session_kind = business`. Each item uses the two-bucket shape (`registry`, `submitted`, `ubo_kyc_summary`).""" + + document_verifications: list[SessionsRetrieveDecisionResponseDocumentVerifications] | None = ( + None + ) + """Document verification checks grouped by workflow node. Present only when `session_kind = business`.""" + + environment: Literal["live", "sandbox"] | str | None = None + """Whether the session was created by a live application (`live`) or a sandbox application (`sandbox`). Sandbox sessions are free, fully simulated, and never bill credits.""" + + resubmissions: list[SessionsRetrieveDecisionResponseResubmissions] | None = None + """Returned only with `include=resubmissions`, for user and business sessions. One entry per resubmission request, oldest first. Each entry carries the results the resubmitted steps returned before the request, so earlier submissions stay readable. Deleting the session deletes every submission.""" diff --git a/src/didit/types/sessions/retrieve_deletion_job_response.py b/src/didit/types/sessions/retrieve_deletion_job_response.py new file mode 100644 index 0000000..47c5f3c --- /dev/null +++ b/src/didit/types/sessions/retrieve_deletion_job_response.py @@ -0,0 +1,62 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsRetrieveDeletionJobResponse", "SessionsRetrieveDeletionJobResponseOutcomes"] + + +class SessionsRetrieveDeletionJobResponseOutcomes(BaseModel): + """How many sessions ended in each outcome. Every key is always present.""" + + deleted: int | None = None + """Sessions deleted together with their face embedding.""" + + none: int | None = None + """Sessions deleted that had no face embedding.""" + + retained_with_user: int | None = None + """Sessions deleted while one image-free biometric template was retained for their User, as the request or the application policy asked.""" + + ineligible_no_vendor_user: int | None = None + """Sessions deleted, with their face embedding, because retention was requested but they had no linked User.""" + + failed_retryable: int | None = None + """Sessions reported as not deleted. A job retries a group it could not delete by itself instead of counting it here.""" + + already_deleted: int | None = None + """Selected sessions that were already deleted, for example by another request, when the job reached them.""" + + +class SessionsRetrieveDeletionJobResponse(BaseModel): + """A bulk session deletion running in the background: progress and counts per outcome, never which sessions it deleted.""" + + job_id: str | None = None + """Poll `GET /v3/sessions/delete/{job_id}/` with this id.""" + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + """`PENDING`: accepted, not started yet. `PROCESSING`: deleting; `processed` and `outcomes` are updating. `COMPLETED`: every selected session is accounted for. `FAILED`: the job stopped early; see `error`.""" + + total: int | None = None + """Sessions selected when the deletion was requested.""" + + processed: int | None = None + """Sessions the job has finished with so far.""" + + outcomes: SessionsRetrieveDeletionJobResponseOutcomes | None = None + """How many sessions ended in each outcome. Every key is always present.""" + + deleted_child_verifications: int | None = None + """Verifications that the deleted business verifications created, deleted with them. Always `0` for a job started by `POST /v3/sessions/delete/`, which deletes KYC sessions only.""" + + error: str | None = None + """`null` unless `status` is `FAILED`; then why the deletion stopped. The sessions the job had not reached are still in place: repeat the request to delete them.""" + + created_at: _dt.datetime | None = None + """When the deletion was requested.""" + + completed_at: _dt.datetime | None = None + """When the job finished; `null` while it runs.""" diff --git a/src/didit/types/sessions/reviews/__init__.py b/src/didit/types/sessions/reviews/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/sessions/reviews/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/sessions/reviews/create_params.py b/src/didit/types/sessions/reviews/create_params.py new file mode 100644 index 0000000..9eb6963 --- /dev/null +++ b/src/didit/types/sessions/reviews/create_params.py @@ -0,0 +1,31 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["SessionsReviewsCreateParams"] + + +class SessionsReviewsCreateParams(TypedDict, total=False): + new_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) + """Optional status label to record on the entry. Accepts any session status value, case-sensitive (`"approved"` is rejected with `400`). Recording a status here does **not** change the session's actual status - use the update-status endpoint for that.""" + + comment: str | None + """Optional free-text note (no enforced length limit). Stored verbatim; `@email` mentions are not processed on this endpoint.""" diff --git a/src/didit/types/sessions/reviews/create_response.py b/src/didit/types/sessions/reviews/create_response.py new file mode 100644 index 0000000..df29240 --- /dev/null +++ b/src/didit/types/sessions/reviews/create_response.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsReviewsCreateResponse"] + + +class SessionsReviewsCreateResponse(BaseModel): + user: str | None = None + """Display name of the entry's author. Always `"Unknown"` for entries created through this endpoint, because it records no actor attribution.""" + + new_status: ( + Literal[ + "Not Started", + "In Progress", + "Approved", + "Declined", + "In Review", + "Expired", + "Abandoned", + "Kyc Expired", + "Resubmitted", + "Awaiting User", + ] + | str + | None + ) = None + """The status label recorded on the entry, or `null` if none was sent. The session's actual status is not affected.""" + + comment: str | None = None + """The note recorded on the entry, or `null` if none was sent.""" + + created_at: _dt.datetime | None = None + """When the entry was written, ISO 8601 UTC.""" diff --git a/src/didit/types/sessions/reviews/list_response.py b/src/didit/types/sessions/reviews/list_response.py new file mode 100644 index 0000000..ece5c3a --- /dev/null +++ b/src/didit/types/sessions/reviews/list_response.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.review_item import ReviewItem + +__all__ = ["SessionsReviewsListResponse"] + + +class SessionsReviewsListResponse(BaseModel): + count: int | None = None + """Total number of feed entries for this session (across all pages).""" + + next: str | None = None + """URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """URL of the previous page, or `null` on the first page.""" + + results: list[ReviewItem] | None = None + """Feed entries for this page.""" diff --git a/src/didit/types/sessions/share_params.py b/src/didit/types/sessions/share_params.py new file mode 100644 index 0000000..06aa4e2 --- /dev/null +++ b/src/didit/types/sessions/share_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsShareParams"] + + +class SessionsShareParams(TypedDict, total=False): + for_application_id: Required[str] + """UUID of the Didit application that will redeem the token. Must exist, must not be deleted, and must differ from the calling application. Find it in the Business Console under **Settings → Application**.""" + + ttl_in_seconds: int + """Token lifetime, in seconds. Minimum `60`, maximum `86400` (24 h). Defaults to `3600` (1 h).""" diff --git a/src/didit/types/sessions/share_response.py b/src/didit/types/sessions/share_response.py new file mode 100644 index 0000000..ec2d251 --- /dev/null +++ b/src/didit/types/sessions/share_response.py @@ -0,0 +1,19 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsShareResponse"] + + +class SessionsShareResponse(BaseModel): + share_token: str | None = None + """HS256-signed JWT. Pass this verbatim as `share_token` to [`POST /v3/session/import-shared/`](/sessions-api/import-shared-session) on the target application.""" + + for_application_id: str | None = None + """Echoes the target application UUID that the token is bound to.""" + + session_kind: Literal["user", "business"] | str | None = None + """Whether the source session is a User Verification (KYC) or Business Verification (KYB) session.""" diff --git a/src/didit/types/sessions/update_aml_hit_status_params.py b/src/didit/types/sessions/update_aml_hit_status_params.py new file mode 100644 index 0000000..f523069 --- /dev/null +++ b/src/didit/types/sessions/update_aml_hit_status_params.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsUpdateAmlHitStatusParams"] + + +class SessionsUpdateAmlHitStatusParams(TypedDict, total=False): + hit_id: Required[str] + """The `id` of the hit in the AML check's `hits` array.""" + + review_status: Required[ + Literal["Unreviewed", "Confirmed Match", "False Positive", "Inconclusive"] | str + ] + """The new review status for the hit.""" + + node_id: str + """Optional graph node ID. Use it (here or as a query parameter) when the session has multiple AML checks.""" diff --git a/src/didit/types/sessions/update_data_params.py b/src/didit/types/sessions/update_data_params.py new file mode 100644 index 0000000..98f3ed8 --- /dev/null +++ b/src/didit/types/sessions/update_data_params.py @@ -0,0 +1,89 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping + +from typing_extensions import TypedDict + +__all__ = ["SessionsUpdateDataParams", "SessionsUpdateDataParamsParsedAddress"] + + +class SessionsUpdateDataParamsParsedAddress(TypedDict, total=False): + """Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response - you cannot set it directly.""" + + address_type: str | None + """Free-form address classification (e.g. `home`).""" + + city: str | None + + region: str | None + """State / province / autonomous community.""" + + street_1: str | None + + street_2: str | None + + postal_code: str | None + + country: str | None + """ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.""" + + +class SessionsUpdateDataParams(TypedDict, total=False): + """All fields optional - send only what you want to change.""" + + document_type: ( + Literal["P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER"] + | str + | None + ) + """Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`.""" + + document_subtype: str | None + """Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants.""" + + document_number: str | None + """Document number as printed on the ID.""" + + personal_number: str | None + """Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport).""" + + date_of_birth: str | _dt.date | None + """Date of birth in `YYYY-MM-DD`. Any other format returns `400`.""" + + date_of_issue: str | _dt.date | None + """Issue date of the document in `YYYY-MM-DD`.""" + + expiration_date: str | _dt.date | None + """Expiration date of the document in `YYYY-MM-DD`.""" + + issuing_state: str | None + """Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`.""" + + first_name: str | None + """Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint.""" + + last_name: str | None + """Family name(s). Also feeds the recomputed `full_name`.""" + + gender: Literal["M", "F", "U"] | str | None + """Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value - including `X` - returns `400`.""" + + address: str | None + """Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it synchronously before responding - the 200 body already reflects the refreshed `parsed_address`. The re-geocode also requires `issuing_state` to be present on the KYC record.""" + + place_of_birth: str | None + """Place of birth as printed on the document.""" + + nationality: str | None + """Nationality as printed on the document. Free text up to 32 characters - typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one.""" + + marital_status: Literal["SINGLE", "MARRIED", "DIVORCED", "WIDOWED", "UNKNOWN"] | str + """Marital status where the document carries it. Any other value returns `400`.""" + + extra_fields: Mapping[str, object] | None + """Per-key overrides for document-specific extra fields (e.g. `blood_group`, `dl_categories`, `mrz_string`). Keys are merged into the existing override map: send a key with a value to set it, or with `null` / `""` to remove that key from the merged view. Send the whole field as `null` to clear all previously applied overrides. Must be a JSON object, otherwise `400`. The response always returns the merged view (OCR-extracted values with your overrides applied).""" + + parsed_address: SessionsUpdateDataParamsParsedAddress | None + """Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` and is returned in the response - you cannot set it directly.""" diff --git a/src/didit/types/sessions/update_data_response.py b/src/didit/types/sessions/update_data_response.py new file mode 100644 index 0000000..73c6b39 --- /dev/null +++ b/src/didit/types/sessions/update_data_response.py @@ -0,0 +1,92 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateDataResponse", "SessionsUpdateDataResponseParsedAddress"] + + +class SessionsUpdateDataResponseParsedAddress(BaseModel): + """Current structured address (with internal bookkeeping keys stripped), or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys.""" + + address_type: str | None = None + """Free-form address classification (e.g. `home`).""" + + city: str | None = None + + region: str | None = None + """State / province / autonomous community.""" + + street_1: str | None = None + + street_2: str | None = None + + postal_code: str | None = None + + country: str | None = None + """ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.""" + + formatted_address: str | None = None + """Joined, human-readable form of the structured address. Recomputed server-side after every `parsed_address` edit.""" + + +class SessionsUpdateDataResponse(BaseModel): + document_type: ( + Literal["P", "DL", "ID", "RP", "SSC", "HIC", "WP", "TC", "VISA", "PSC", "BC", "OTHER"] + | str + | None + ) = None + """Document type code. Accepts the short codes listed in the enum, plus six long-form aliases normalized server-side before validation: `Passport` → `P`, `Identity Card` → `ID`, `Driver's License` → `DL`, `Residence Permit` → `RP`, `Health Insurance Card` → `HIC`, `Tax Card` → `TC`. Any other value (including `PASSPORT` in caps) returns `400`.""" + + document_subtype: str | None = None + """Document subtype from the document registry, including the region prefix when the registry distinguishes regional variants.""" + + document_number: str | None = None + """Document number as printed on the ID.""" + + personal_number: str | None = None + """Personal / national identification number where the document carries one (e.g. Spanish DNI on a passport).""" + + date_of_birth: _dt.date | None = None + """Date of birth in `YYYY-MM-DD`. Any other format returns `400`.""" + + date_of_issue: _dt.date | None = None + """Issue date of the document in `YYYY-MM-DD`.""" + + expiration_date: _dt.date | None = None + """Expiration date of the document in `YYYY-MM-DD`.""" + + issuing_state: str | None = None + """Issuing country as an ISO 3166-1 alpha-3 code. Case-insensitive (`esp` is stored as `ESP`); the human-readable `issuing_state_name` shown in [Get Decision](#get-/v3/session/-sessionId-/decision/) is recomputed from this value. Invalid codes return `400`.""" + + first_name: str | None = None + """Given name(s). The stored `full_name` is recomputed as `first_name + ' ' + last_name` on every call to this endpoint.""" + + last_name: str | None = None + """Family name(s). Also feeds the recomputed `full_name`.""" + + gender: Literal["M", "F", "U"] | str | None = None + """Sex as printed on the document: `M` (male), `F` (female), or `U` (unknown). Any other value - including `X` - returns `400`.""" + + address: str | None = None + """Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `parsed_address` with the geocoder result.""" + + place_of_birth: str | None = None + """Place of birth as printed on the document.""" + + nationality: str | None = None + """Nationality as printed on the document. Free text up to 32 characters - typically an ISO 3166-1 alpha-3 code (e.g. `ESP`), but not validated as one.""" + + marital_status: Literal["SINGLE", "MARRIED", "DIVORCED", "WIDOWED", "UNKNOWN"] | str | None = ( + None + ) + """Marital status where the document carries it. Any other value returns `400`.""" + + extra_fields: dict[str, object] | None = None + """Merged view of the document's extra fields: the OCR-extracted values with all operator overrides applied. Keys removed via a `null` override are absent from this map.""" + + parsed_address: SessionsUpdateDataResponseParsedAddress | None = None + """Current structured address (with internal bookkeeping keys stripped), or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys.""" diff --git a/src/didit/types/sessions/update_feature_status_params.py b/src/didit/types/sessions/update_feature_status_params.py new file mode 100644 index 0000000..d1de0d0 --- /dev/null +++ b/src/didit/types/sessions/update_feature_status_params.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsUpdateFeatureStatusParams"] + + +class SessionsUpdateFeatureStatusParams(TypedDict, total=False): + new_status: Required[Literal["Approved", "Declined", "In Review"] | str] + """Target status for this feature. Must differ from the current value.""" + + comment: str + """Optional free-text note recorded on the session's review trail for this feature change.""" diff --git a/src/didit/types/sessions/update_feature_status_response.py b/src/didit/types/sessions/update_feature_status_response.py new file mode 100644 index 0000000..569e020 --- /dev/null +++ b/src/didit/types/sessions/update_feature_status_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateFeatureStatusResponse"] + + +class SessionsUpdateFeatureStatusResponse(BaseModel): + session_id: str | None = None + + node_id: str | None = None + + feature_type: str | None = None + """Model name of the updated feature (e.g. `KYC`, `Face`, `FaceMatch`, `AML`, `POA`).""" + + new_status: str | None = None diff --git a/src/didit/types/sessions/update_kyb_company_data_params.py b/src/didit/types/sessions/update_kyb_company_data_params.py new file mode 100644 index 0000000..300cd04 --- /dev/null +++ b/src/didit/types/sessions/update_kyb_company_data_params.py @@ -0,0 +1,101 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["SessionsUpdateKybCompanyDataParams"] + + +class SessionsUpdateKybCompanyDataParams(TypedDict, total=False): + """All fields optional - send only what you want to change. Provided fields update the canonical company columns (where one exists) and are merged into the `registry_data` snapshot.""" + + company_name: str + """Legal name of the company. Written to the canonical column and the registry snapshot.""" + + registration_number: str + """Official registration number with the company registry.""" + + country_code: str + """Country of incorporation, ISO 3166-1 alpha-2. Case-insensitive (`us` is stored as `US`). Alpha-3 codes fail the 2-character limit with `400`.""" + + region: str | None + """Subdivision / region tag where the registry has one (e.g. US state). Stored in the registry snapshot only - it has no canonical column.""" + + incorporation_date: str | _dt.date + """Incorporation date in `YYYY-MM-DD`. Optional like every other field - the `PATCH` is partial. Any other date format returns `400`.""" + + registration_date: str | _dt.date | None + """Registration date where the registry distinguishes it from incorporation. Stored in the registry snapshot only.""" + + dissolution_date: str | _dt.date | None + """Dissolution date for companies that are no longer active. Stored in the registry snapshot only.""" + + tax_number: str + """Tax identification number.""" + + legal_address: str + """Legal / registered address. Aliased server-side to the canonical `registered_address` column - the response returns it under `registered_address`, while the registry snapshot keeps the `legal_address` key you sent.""" + + company_type: str + """Legal entity type (e.g. `Limited Liability Company`, `PLC`, `GmbH`).""" + + registry_status: ( + Literal[ + "active", + "dissolved", + "deregistered", + "see full details", + "authorised", + "appointed representative", + "unauthorised", + "inactive", + "no longer authorised", + "closed", + "struck off", + ] + | str + | None + ) + """Registry-reported company status. Lowercase, some values contain spaces (e.g. `struck off`, `no longer authorised`). Any other value returns `400`.""" + + verification_status: Literal["verified", "failed", "unknown"] | str | None + """Outcome of the registry verification. Any other value returns `400`.""" + + alternative_names: str + """Trade names, former legal names, DBA names (free text).""" + + nature_of_business: str + """Short description of the company's activity.""" + + registered_capital: str + """Human-readable registered capital (e.g. `USD 50000`). Use `registered_capital_amount` + `registered_capital_currency` for structured values, or this field for free-form text.""" + + registered_capital_amount: float | None + """Numeric registered-capital amount (up to 20 digits, 2 decimal places). Sets the `registered_capital_amount` column and is mirrored into `registry_data` as a decimal string; the response echoes it as a decimal string (e.g. `"1500000.50"`).""" + + registered_capital_currency: str | None + """ISO 4217 currency code for `registered_capital_amount`. Case-insensitive (`usd` is stored as `USD`); values that are not exactly 3 letters return `400` with the ISO 4217 message. Sets the canonical `registered_capital_currency` column and is mirrored into `registry_data`.""" + + website: str + """Company website. Bare domains are accepted and normalized with an `https://` prefix (`docsprobe.example.com` is stored as `https://docsprobe.example.com`); values that still fail URL validation return `400`.""" + + email: str + """Contact email on file with the registry.""" + + phone: str + """Contact phone. Must match `+`, digits, spaces, dashes, dots, or parentheses, with 7-15 digits total - otherwise `400`.""" + + legal_entity_identifier: str + """LEI, where the company has one.""" + + location_of_registration: str + """City / subdivision of the registry of record.""" + + vat_number: str + """EU VAT number. Merged into the `registry_data` snapshot only - editing it here does not re-run VAT and tax ID validation, so `vat_validation_status` and the other `vat_*` result fields are unchanged.""" + + control_scheme: str + """Control / governance scheme when the registry exposes it. Stored in the registry snapshot only.""" diff --git a/src/didit/types/sessions/update_kyb_company_data_response.py b/src/didit/types/sessions/update_kyb_company_data_response.py new file mode 100644 index 0000000..3f89e4b --- /dev/null +++ b/src/didit/types/sessions/update_kyb_company_data_response.py @@ -0,0 +1,159 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateKybCompanyDataResponse"] + + +class SessionsUpdateKybCompanyDataResponse(BaseModel): + """Full KYB company record - the same shape as the `company` object inside `registry_checks[]` on the business session decision.""" + + uuid: str | None = None + """Company record identifier (the `companyUuid` path parameter).""" + + node_id: str | None = None + """Workflow graph node this company record belongs to, when the workflow is graph-based.""" + + status: str | None = None + """Feature lifecycle status of the registry check (e.g. `Not Finished`, `Approved`, `Declined`, `In Review`).""" + + registry_status: str | None = None + """Registry-reported company status (e.g. `active`, `dissolved`, `struck off`).""" + + data_resolved: bool | None = None + """`true` when the company data is complete - always `true` for manual-entry companies; for registry-sourced companies, `true` once the registry fetch resolved.""" + + company_name: str | None = None + + registration_number: str | None = None + + country_code: str | None = None + """ISO 3166-1 alpha-2 country of incorporation.""" + + region: str | None = None + + company_type: str | None = None + + incorporation_date: _dt.date | None = None + + registered_address: str | None = None + """Canonical registered address - updated when you send `legal_address`.""" + + tax_number: str | None = None + + risk_level: str | None = None + + verification_status: str | None = None + """`verified`, `failed`, or `unknown`.""" + + is_from_registry: bool | None = None + """`true` when the company was pre-filled from a registry search rather than entered manually.""" + + fetch_status: str | None = None + """Registry fetch lifecycle for registry-sourced companies (`pending` / `resolved`); `null` for manual entries.""" + + alternative_names: str | None = None + """Alternative, trading, former or original-language company names as text, when reported.""" + + nature_of_business: str | None = None + + registered_capital: str | None = None + + registered_capital_amount: str | None = None + """Canonical column. Returned as a decimal string (e.g. `"2750000.00"`).""" + + registered_capital_currency: str | None = None + """Canonical column. Set by the registry flow or by console/API edits via this endpoint.""" + + website: str | None = None + + email: str | None = None + + phone: str | None = None + + legal_entity_identifier: str | None = None + + location_of_registration: str | None = None + + vat_number: str | None = None + """EU VAT number, when collected during the hosted registry step or edited afterwards.""" + + vat_validation_status: ( + Literal[ + "valid", + "invalid", + "format_valid", + "invalid_format", + "could_not_validate", + "not_applicable", + ] + | str + | None + ) = None + """Result of validating the VAT number or, when there is none, the `tax_number`, at registry submit. `valid` / `invalid`: decided by the official registry of the country (EU VIES, Norway, Switzerland, Brazil, Colombia, Costa Rica, Ecuador, Paraguay; Poland also by its domestic VAT register). `format_valid` / `invalid_format`: decided by the country's format and check-digit rules where no registry can be queried. `could_not_validate`: the registry was unavailable. `not_applicable`: no number, a country without registry or rules, or a sandbox session.""" + + vat_validated_number: str | None = None + """The number that was checked, as the source knows it (for example the VAT number derived from a French SIREN or a Dutch RSIN). `null` when no check ran.""" + + vat_validation_source: ( + Literal[ + "eu_vies", + "pl_vat_white_list", + "no_brreg", + "ch_uid_register", + "br_receita_federal", + "co_rues", + "cr_hacienda", + "ec_sri", + "py_dnit", + "format_rules", + ] + | str + | None + ) = None + """The source that decided `vat_validation_status`: an official registry, or `format_rules`. `null` when no check ran.""" + + vat_validated_name: str | None = None + """Name registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_validated_address: str | None = None + """Address registered with the source that validated the number, when it discloses one. Only set when the status is `valid`.""" + + vat_checked_at: _dt.datetime | None = None + """Timestamp of the check. `null` when no check ran.""" + + financial_summary: dict[str, object] | None = None + + officers: list[dict[str, object]] | None = None + """Directors / officers attached to the company, including per-person KYC progress.""" + + beneficial_owners: list[dict[str, object]] | None = None + """Beneficial owners attached to the company, including per-person KYC progress.""" + + addresses: list[dict[str, object]] | None = None + """Registry address detail, when the registry returned any.""" + + industries: list[dict[str, object]] | None = None + """Registry industry classification, when available.""" + + accounts: dict[str, object] | None = None + """Registry filing dates, when available. Annual returns and general meetings use last_annual_return_date and last_agm_date; they are distinct from financial accounts.""" + + registry_data: dict[str, object] | None = None + """Raw registry payload snapshot. Every key you PATCH is merged into this map (dates serialized as ISO strings), so the "extracted from registry" view reflects operator corrections.""" + + user_provided_data: dict[str, object] | None = None + """What the end user typed during the hosted registry-confirmation step. Never modified by this endpoint.""" + + confirmed_by_user_at: _dt.datetime | None = None + """When the end user confirmed the company data in the hosted flow.""" + + last_console_edit_at: _dt.datetime | None = None + """Stamped on every successful non-empty PATCH, so UIs can surface "edited by operator" context.""" + + is_editable: bool | None = None + """`true` while the end user can still edit the registry-sourced data in the hosted flow (registry-sourced and not yet confirmed).""" diff --git a/src/didit/types/sessions/update_kyb_party_params.py b/src/didit/types/sessions/update_kyb_party_params.py new file mode 100644 index 0000000..d9a2c79 --- /dev/null +++ b/src/didit/types/sessions/update_kyb_party_params.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsUpdateKybPartyParams"] + + +class SessionsUpdateKybPartyParams(TypedDict, total=False): + is_skipped: Required[bool] + """`true` exempts the party from verification so the Key People check can settle without it; `false` puts it back in scope.""" diff --git a/src/didit/types/sessions/update_kyb_party_response.py b/src/didit/types/sessions/update_kyb_party_response.py new file mode 100644 index 0000000..796097b --- /dev/null +++ b/src/didit/types/sessions/update_kyb_party_response.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateKybPartyResponse"] + + +class SessionsUpdateKybPartyResponse(BaseModel): + """Key People party record - the same shape as an entry of `key_people_checks[].submitted.parties[]` on the business session decision.""" + + uuid: str | None = None + """Party identifier.""" + + entity_type: Literal["person", "company"] | str | None = None + """Whether the party is an individual or a company.""" + + name: str | None = None + """Display name of the party.""" + + is_skipped: bool | None = None + """Whether the party is exempt from verification.""" + + requires_verification: bool | None = None + """What the role and threshold rules asked for. Left untouched by this endpoint.""" + + kyc_session_status: str | None = None + """Status of the linked individual KYC session, frozen at deletion time when the session was deleted.""" + + kyc_session_url: str | None = None + """Link to the linked individual KYC session. `null` when there is none or when it was deleted.""" + + kyc_session_deleted: bool | None = None + """The linked individual KYC session was deleted, so it can never settle.""" + + kyb_sub_session_deleted: bool | None = None + """Corporate-party counterpart of `kyc_session_deleted`.""" diff --git a/src/didit/types/sessions/update_poa_data_params.py b/src/didit/types/sessions/update_poa_data_params.py new file mode 100644 index 0000000..dab8b70 --- /dev/null +++ b/src/didit/types/sessions/update_poa_data_params.py @@ -0,0 +1,70 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping + +from typing_extensions import TypedDict + +__all__ = ["SessionsUpdatePoaDataParams", "SessionsUpdatePoaDataParamsPoaParsedAddress"] + + +class SessionsUpdatePoaDataParamsPoaParsedAddress(TypedDict, total=False): + """Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` - you cannot set it directly.""" + + address_type: str | None + """Free-form address classification (e.g. `home`).""" + + city: str | None + + region: str | None + """State / province / autonomous community.""" + + street_1: str | None + + street_2: str | None + + postal_code: str | None + + country: str | None + """ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.""" + + +class SessionsUpdatePoaDataParams(TypedDict, total=False): + """All fields optional - send only what you want to change.""" + + issuing_state: str | None + """Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`.""" + + document_type: ( + Literal[ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN", + ] + | str + ) + """POA document category. Values are uppercase - lowercase variants like `utility_bill` return `400`.""" + + document_language: str | None + """Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text - not validated as a language code.""" + + issuer: str | None + """Organization that issued the document (utility company, bank, government agency).""" + + issue_date: str | _dt.date | None + """Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`.""" + + poa_address: str | None + """Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result. The re-geocode also requires `issuing_state` to be present on the POA record.""" + + name_on_document: str | None + """Recipient name as printed on the document.""" + + extra_fields: Mapping[str, object] | None + """Document-specific extras, merged key-by-key. Nine keys map to dedicated POA columns: `bank_account_number`, `bank_iban`, `bank_sort_code`, `bank_routing_number`, `bank_swift_bic`, `bank_branch_name`, `bank_branch_address`, `document_phone_number`, and `additional_names` (must be a list of strings or `null`, otherwise `400`). Any other key is stored as a custom extra field. Per-key semantics: a value sets the key; `null` or `""` deletes it. Must be a JSON object, otherwise `400`.""" + + poa_parsed_address: SessionsUpdatePoaDataParamsPoaParsedAddress | None + """Structured address. Only these nested keys are accepted: `address_type`, `city`, `region`, `street_1`, `street_2`, `postal_code`, `country` - any other key returns `400`. Per-key semantics: a value sets the key, `null` removes it. `formatted_address` (and the top-level `poa_formatted_address`) is recomputed server-side by joining `street_1`, `street_2`, `city`, `region`, `postal_code`, and `country` - you cannot set it directly.""" diff --git a/src/didit/types/sessions/update_poa_data_response.py b/src/didit/types/sessions/update_poa_data_response.py new file mode 100644 index 0000000..2dbeda3 --- /dev/null +++ b/src/didit/types/sessions/update_poa_data_response.py @@ -0,0 +1,75 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = ["SessionsUpdatePoaDataResponse", "SessionsUpdatePoaDataResponsePoaParsedAddress"] + + +class SessionsUpdatePoaDataResponsePoaParsedAddress(BaseModel): + """Current structured address, or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys. Returned as stored - after a server-side geocode it can contain bookkeeping keys (e.g. `raw_results`) alongside the editable keys.""" + + address_type: str | None = None + """Free-form address classification (e.g. `home`).""" + + city: str | None = None + + region: str | None = None + """State / province / autonomous community.""" + + street_1: str | None = None + + street_2: str | None = None + + postal_code: str | None = None + + country: str | None = None + """ISO 3166-1 alpha-2 country code. Lowercase input is normalized to uppercase; alpha-3 codes are rejected with `400`.""" + + formatted_address: str | None = None + """Joined, human-readable form of the structured address. Recomputed server-side after every `poa_parsed_address` edit.""" + + +class SessionsUpdatePoaDataResponse(BaseModel): + issuing_state: str | None = None + """Country that issued the POA document, ISO 3166-1 alpha-3. Case-insensitive (normalized to uppercase); invalid codes return `400`.""" + + document_type: ( + Literal[ + "UTILITY_BILL", + "BANK_STATEMENT", + "GOVERNMENT_ISSUED_DOCUMENT", + "OTHER_POA_DOCUMENT", + "UNKNOWN", + ] + | str + | None + ) = None + """POA document category. Values are uppercase - lowercase variants like `utility_bill` return `400`.""" + + document_language: str | None = None + """Language of the document, typically an ISO 639-1 code (e.g. `es`). Free text - not validated as a language code.""" + + issuer: str | None = None + """Organization that issued the document (utility company, bank, government agency).""" + + issue_date: _dt.date | None = None + """Issue date of the POA document in `YYYY-MM-DD`. Any other format returns `400`.""" + + poa_address: str | None = None + """Free-text address as printed on the document. When this value changes (non-sandbox sessions only) and the structured address is still missing or incomplete, the server re-geocodes it in the background of the same request and may overwrite `poa_parsed_address` / `poa_formatted_address` with the geocoder result.""" + + name_on_document: str | None = None + """Recipient name as printed on the document.""" + + extra_fields: dict[str, object] | None = None + """Always present in the response: the nine known keys (with `null` for unset ones, `additional_names` defaulting to `[]`) plus any custom keys previously stored.""" + + poa_parsed_address: SessionsUpdatePoaDataResponsePoaParsedAddress | None = None + """Current structured address, or `null` when none exists. Includes the recomputed `formatted_address` alongside the seven editable keys. Returned as stored - after a server-side geocode it can contain bookkeeping keys (e.g. `raw_results`) alongside the editable keys.""" + + poa_formatted_address: str | None = None + """Top-level copy of the recomputed `formatted_address`, kept in sync when the structured address changes.""" diff --git a/src/didit/types/sessions/update_status_params.py b/src/didit/types/sessions/update_status_params.py new file mode 100644 index 0000000..e0ddf01 --- /dev/null +++ b/src/didit/types/sessions/update_status_params.py @@ -0,0 +1,67 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["SessionsUpdateStatusParams", "SessionsUpdateStatusParamsNodesToResubmit"] + + +class SessionsUpdateStatusParamsNodesToResubmit(TypedDict, total=False): + node_id: Required[str] + """Node identifier as it appears in the session's workflow definition (for example `feature_ocr`, `feature_liveness`).""" + + feature: Required[ + Literal[ + "OCR", + "OCR_BACK", + "NFC", + "AML", + "FACE", + "LIVENESS", + "FACE_MATCH", + "IP_ANALYSIS", + "AGE_ESTIMATION", + "PROOF_OF_ADDRESS", + "PHONE_VERIFICATION", + "EMAIL_VERIFICATION", + "FACE_SEARCH", + "DATABASE_VALIDATION", + "QUESTIONNAIRE", + "DOCUMENT_AI", + "KYB_DOCUMENTS", + "ID_VERIFICATION", + "POA", + "PHONE", + "EMAIL", + "KYB_REGISTRY", + "KYB_KEY_PEOPLE", + "KYB", + ] + | str + ] + """Feature type of the node. The aliases `ID_VERIFICATION`, `POA`, `PHONE`, and `EMAIL` are normalized server-side to `OCR`, `PROOF_OF_ADDRESS`, `PHONE_VERIFICATION`, and `EMAIL_VERIFICATION`. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and `KYB` pass schema validation but are always rejected with the business-rule `400` shown in the examples.""" + + +class SessionsUpdateStatusParams(TypedDict, total=False): + new_status: Required[Literal["Approved", "Declined", "Resubmitted"] | str] + """Target status. `Approved` and `Declined` record a final manual decision (each can also overturn the other). `Resubmitted` clears the selected steps and sends the session back to the user. Any other value returns `400`.""" + + comment: str + """Free-text reason for the change, stored on the session's review trail and returned in the `reviews` array of [Get Decision](#get-/v3/session/-sessionId-/decision/). For example `Duplicated user`.""" + + nodes_to_resubmit: Sequence[SessionsUpdateStatusParamsNodesToResubmit] + """Workflow steps the user must redo. Only acted on when `new_status` is `Resubmitted`. For `Approved`/`Declined` the entries are still schema-validated (an invalid `feature` value returns `400`) but schema-valid entries are semantically ignored. If omitted, the server auto-selects existing OCR, Liveness, Face Match, POA, Phone, Email, AML, Database Validation, and Questionnaire attempts whose status is `Declined`, `In Review`, `Not Finished`, or `Expired` (NFC, IP analysis, age estimation, face search, and KYB document attempts are never auto-selected; non-face-match face attempts are selected as `LIVENESS`) - features the user never attempted (no recorded attempt) are NOT selected, so a session with zero attempts returns `400` ("No features found that need resubmission") even though nothing was approved; pass `nodes_to_resubmit` explicitly in that case. Steps are executed in workflow-graph order regardless of the order you send them. `KYB_REGISTRY`, `KYB_KEY_PEOPLE`, and the `KYB` alias are rejected with `400` - those parent checks recompute from their child KYC sessions.""" + + send_email: bool + """Whether to email the user about the change. Requires `email_address`. For `Approved`/`Declined` the user receives a status notice; for `Resubmitted` the email includes the verification link and the per-step resubmission reasons.""" + + email_address: str + """Recipient for the notification email. **Required when `send_email` is `true`** - omitting it returns `400`.""" + + email_language: str + """Language for the notification email. Accepts any string at schema level; unsupported codes silently fall back to English (`en`).""" + + email_preview_token: str + """Optional signed receipt from Preview Resubmission Email. When supplied with send_email=true and new_status=Resubmitted, the recipient, language, checks, current session state, sender and message must still match the preview. A changed or expired receipt returns 409 before changing the session or sending email. Existing integrations may omit this field.""" diff --git a/src/didit/types/sessions/update_status_response.py b/src/didit/types/sessions/update_status_response.py new file mode 100644 index 0000000..ab9a968 --- /dev/null +++ b/src/didit/types/sessions/update_status_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["SessionsUpdateStatusResponse"] + + +class SessionsUpdateStatusResponse(BaseModel): + session_id: str | None = None + """UUID of the updated session.""" diff --git a/src/didit/types/transactions/__init__.py b/src/didit/types/transactions/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/transactions/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/transactions/create_params.py b/src/didit/types/transactions/create_params.py new file mode 100644 index 0000000..83976ac --- /dev/null +++ b/src/didit/types/transactions/create_params.py @@ -0,0 +1,368 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = [ + "TransactionsCreateParams", + "TransactionsCreateParamsCounterparty", + "TransactionsCreateParamsCounterpartyDeviceContext", + "TransactionsCreateParamsCounterpartyDeviceContextNetworkContext", + "TransactionsCreateParamsCounterpartyPaymentMethod", + "TransactionsCreateParamsNetworkSnapshot", + "TransactionsCreateParamsSubject", + "TransactionsCreateParamsSubjectAddress", + "TransactionsCreateParamsSubjectDeviceContext", + "TransactionsCreateParamsSubjectDeviceContextNetworkContext", + "TransactionsCreateParamsSubjectInstitutionDetails", + "TransactionsCreateParamsSubjectPaymentMethod", + "TransactionsCreateParamsTransactionDetails", + "TransactionsCreateParamsTransactionDetailsCryptoParams", + "TransactionsCreateParamsTravelRuleDetails", +] + + +class TransactionsCreateParamsCounterpartyDeviceContextNetworkContext(TypedDict, total=False): + """Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.""" + + ip_address: str + """Client IP address. CamelCase alias: ip.""" + + ip_country_code: str + """Geo-resolved country of the IP (ISO 3166-1 alpha-3). CamelCase alias: countryCode3.""" + + latitude: float + """Geographic latitude of the IP. CamelCase alias: lat.""" + + longitude: float + """Geographic longitude of the IP. CamelCase alias: lon.""" + + +class TransactionsCreateParamsCounterpartyDeviceContext(TypedDict, total=False): + """Device context for the counterparty. browser_family and platform must be sent as explicit top-level keys - they are not derived from user_agent.""" + + fingerprint: str + """Unique device fingerprint.""" + + persistent_device_id: str + """Stable cross-session device identifier (v2 fingerprint schema).""" + + user_agent: str + """Browser or app user agent string. CamelCase alias: userAgent.""" + + browser_family: str + """Browser family (e.g. Chrome, Firefox, Safari). Must be sent explicitly - it is not derived from user_agent. CamelCase alias: browserFamily.""" + + browser_version: str + """Browser version string. Must be sent explicitly. CamelCase alias: browserVersion.""" + + platform: str + """Platform identifier (web, ios, android, macos, windows, linux). Must be sent explicitly - it is not derived from user_agent.""" + + os_family: str + """Operating system family (e.g. iOS, Android, Windows). CamelCase alias: osFamily.""" + + os_version: str + """Operating system version string. CamelCase alias: osVersion.""" + + accept_language: str + """Accept-Language header value. CamelCase alias: acceptLang.""" + + session_id: str + """Session identifier for the counterparty session. Stored in metadata. CamelCase alias: sessionId.""" + + session_age_ms: float + """Age of the user session in milliseconds at transaction time. CamelCase alias: sessionAgeMs.""" + + network_context: TransactionsCreateParamsCounterpartyDeviceContextNetworkContext + """Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.""" + + +class TransactionsCreateParamsCounterpartyPaymentMethod(TypedDict, total=False): + """Payment method used by the counterparty. On travelRule transactions, `method_type: "unhosted_wallet"` declares the counterparty wallet as self-hosted: the transfer skips counterparty-VASP routing, is created directly in `UNCONFIRMED_OWNERSHIP`, and (when `auto_wallet_verification` is enabled) a wallet-ownership widget session is auto-minted and returned in the transaction's `action_required` block.""" + + method_type: str + """Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`. Use `unhosted_wallet` to declare a self-hosted wallet for the Travel Rule.""" + + account_id: str + """Account identifier (e.g. IBAN or wallet address).""" + + issuing_country: str + + +class TransactionsCreateParamsCounterparty(TypedDict, total=False): + """The counterparty of the transaction (optional).""" + + entity_type: str + """Entity type: `individual` or `company`.""" + + vendor_data: str + """Your internal identifier for the counterparty.""" + + full_name: str + """Full name of the counterparty.""" + + first_name: str + + last_name: str + + date_of_birth: str | _dt.date + + address: Mapping[str, object] + """Address object with `country` (ISO 3166-1 alpha-3).""" + + institution_details: Mapping[str, object] + + device_context: TransactionsCreateParamsCounterpartyDeviceContext + """Device context for the counterparty. browser_family and platform must be sent as explicit top-level keys - they are not derived from user_agent.""" + + payment_method: TransactionsCreateParamsCounterpartyPaymentMethod + """Payment method used by the counterparty. On travelRule transactions, `method_type: "unhosted_wallet"` declares the counterparty wallet as self-hosted: the transfer skips counterparty-VASP routing, is created directly in `UNCONFIRMED_OWNERSHIP`, and (when `auto_wallet_verification` is enabled) a wallet-ownership widget session is auto-minted and returned in the transaction's `action_required` block.""" + + +class TransactionsCreateParamsNetworkSnapshot(TypedDict, total=False): + """Pre-computed network graph snapshot.""" + + nodes: Sequence[Mapping[str, object]] + + edges: Sequence[Mapping[str, object]] + + metrics: Mapping[str, object] + + +class TransactionsCreateParamsSubjectAddress(TypedDict, total=False): + """Address object.""" + + country: str + """Country code (ISO 3166-1 alpha-3 recommended). Copied to the party's `country_code`.""" + + town: str + + state: str + + street: str + + post_code: str + + +class TransactionsCreateParamsSubjectDeviceContextNetworkContext(TypedDict, total=False): + """Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.""" + + ip_address: str + """Client IP address. CamelCase alias: ip.""" + + ip_country_code: str + """Geo-resolved country of the IP (ISO 3166-1 alpha-3). CamelCase alias: countryCode3.""" + + latitude: float + """Geographic latitude of the IP. CamelCase alias: lat.""" + + longitude: float + """Geographic longitude of the IP. CamelCase alias: lon.""" + + +class TransactionsCreateParamsSubjectDeviceContext(TypedDict, total=False): + """Device context captured at transaction time. browser_family and platform must be sent as explicit top-level keys - they are not derived from user_agent. The camelCase alias device is also accepted on the request.""" + + fingerprint: str + """Unique device fingerprint.""" + + persistent_device_id: str + """Stable cross-session device identifier (v2 fingerprint schema).""" + + user_agent: str + """Browser or app user agent string. CamelCase alias: userAgent.""" + + browser_family: str + """Browser family (e.g. Chrome, Firefox, Safari). Must be sent explicitly - it is not derived from user_agent. CamelCase alias: browserFamily.""" + + browser_version: str + """Browser version string. Must be sent explicitly. CamelCase alias: browserVersion.""" + + platform: str + """Platform identifier (web, ios, android, macos, windows, linux). Must be sent explicitly - it is not derived from user_agent.""" + + os_family: str + """Operating system family (e.g. iOS, Android, Windows). CamelCase alias: osFamily.""" + + os_version: str + """Operating system version string. CamelCase alias: osVersion.""" + + accept_language: str + """Accept-Language header value. CamelCase alias: acceptLang.""" + + session_id: str + """Session identifier of the user current session. Stored in metadata. CamelCase alias: sessionId.""" + + session_age_ms: float + """Age of the user session in milliseconds at transaction time. CamelCase alias: sessionAgeMs.""" + + network_context: TransactionsCreateParamsSubjectDeviceContextNetworkContext + """Network context with IP address and geo-location. A top-level ipInfo block is also folded into this object.""" + + +class TransactionsCreateParamsSubjectInstitutionDetails(TypedDict, total=False): + """Institution details (for institutional subjects).""" + + name: str + + code: str + + address: Mapping[str, object] + + +class TransactionsCreateParamsSubjectPaymentMethod(TypedDict, total=False): + """Payment method used by the subject.""" + + method_type: str + """Type of payment method: `bank_card`, `bank_account`, `crypto_wallet`, `ewallet`, `unhosted_wallet`, `other`.""" + + account_id: str + """Account identifier (e.g. IBAN, wallet address). For crypto screening, this should contain the wallet address for this participant. Inbound transaction-hash screening requires the service/customer deposit address on the subject. Outbound screening uses the destination wallet on the counterparty.""" + + issuing_country: str + """ISO 3166-1 alpha-3 country code of the issuer.""" + + +class TransactionsCreateParamsSubject(TypedDict, total=False): + """The subject (applicant) of the transaction.""" + + entity_type: str + """Entity type: `individual` or `company`.""" + + vendor_data: Required[str] + """Your internal user/business identifier.""" + + full_name: Required[str] + """Full name of the subject.""" + + first_name: str + """First name of the subject.""" + + last_name: str + """Last name of the subject.""" + + date_of_birth: str | _dt.date + """Date of birth (YYYY-MM-DD).""" + + address: TransactionsCreateParamsSubjectAddress + """Address object.""" + + institution_details: TransactionsCreateParamsSubjectInstitutionDetails + """Institution details (for institutional subjects).""" + + device_context: TransactionsCreateParamsSubjectDeviceContext + """Device context captured at transaction time. browser_family and platform must be sent as explicit top-level keys - they are not derived from user_agent. The camelCase alias device is also accepted on the request.""" + + payment_method: TransactionsCreateParamsSubjectPaymentMethod + """Payment method used by the subject.""" + + +class TransactionsCreateParamsTransactionDetailsCryptoParams(TypedDict, total=False): + """Crypto network selection, separate from currency (the asset symbol). Recording a custom token symbol does not guarantee transaction-hash coverage for its contract.""" + + crypto_chain: str + """Actual network used for screening. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism) and ARB (Arbitrum One). Always specify this for multi-network tokens. Connected-provider coverage varies; LIGHTNING requires a supported connected provider.""" + + +class TransactionsCreateParamsTransactionDetails(TypedDict, total=False): + """Core financial details of the transaction.""" + + direction: Required[Literal["inbound", "outbound", "in", "out"] | str] + """Direction of the transaction relative to the subject. Case-insensitive; `in`/`out` are accepted as aliases. Echoed back uppercase (`INBOUND`/`OUTBOUND`).""" + + amount: Required[float] + """Transaction amount (up to 18 decimal places). Send it as a decimal string (e.g. `"0.123456789012345678"`) when exact precision matters: a JSON number is parsed as a binary float first, so digits beyond about 15 significant digits are rounded before the amount is stored.""" + + currency: Required[str] + """Currency code. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`.""" + + currency_kind: str + """Whether the currency is `fiat` or `crypto`.""" + + amount_in_default_currency: float | None + """Pre-converted amount in the default currency. If omitted, automatic FX conversion is attempted.""" + + default_currency: str + """The default currency code for the converted amount. Any currently-circulating ISO 4217 code.""" + + payment_details: str | None + """Free-text payment reference or memo.""" + + payment_reference_id: str | None + """External payment system reference. For crypto AML transaction-hash screening, send the blockchain transaction hash and also provide `subject.payment_method.account_id` with the related address.""" + + action_type: str | None + """Sub-type of the transaction, e.g. `deposit`, `withdrawal`, `transfer`.""" + + crypto_params: TransactionsCreateParamsTransactionDetailsCryptoParams + """Crypto network selection, separate from currency (the asset symbol). Recording a custom token symbol does not guarantee transaction-hash coverage for its contract.""" + + +class TransactionsCreateParamsTravelRuleDetails(TypedDict, total=False): + """Travel Rule compliance metadata.""" + + status: Required[str] + + protocol: str + + required: bool + + obligations_count: int + + originator_data: Mapping[str, object] + + beneficiary_data: Mapping[str, object] + + metadata: Mapping[str, object] + + +class TransactionsCreateParams(TypedDict, total=False): + transaction_id: Required[str] + """Your unique identifier for this transaction (max 128 characters).""" + + transaction_at: str | _dt.datetime + """When the transaction occurred. Defaults to now if omitted.""" + + time_zone: str + """IANA time zone identifier, e.g. `Europe/Madrid`.""" + + transaction_category: Required[ + Literal[ + "finance", + "kyc", + "travel_rule", + "user_event", + "audit_trail_event", + "gambling_bet", + "gambling_limit_change", + "gambling_bonus_change", + "travelRule", + ] + | str + ] + """Category of the transaction. CamelCase aliases (`travelRule`, `userPlatformEvent`, `gamblingBet`, …) are also accepted; the stored/echoed value is camelCase for multi-word categories.""" + + transaction_details: Required[TransactionsCreateParamsTransactionDetails] + """Core financial details of the transaction.""" + + subject: Required[TransactionsCreateParamsSubject] + """The subject (applicant) of the transaction.""" + + counterparty: TransactionsCreateParamsCounterparty + """The counterparty of the transaction (optional).""" + + custom_properties: Mapping[str, object] + """Arbitrary key-value pairs attached to the transaction. Each key can be referenced in rule conditions with the field path `custom_values.`.""" + + travel_rule_details: TransactionsCreateParamsTravelRuleDetails + """Travel Rule compliance metadata.""" + + network_snapshot: TransactionsCreateParamsNetworkSnapshot + """Pre-computed network graph snapshot.""" + + include_crypto_screening: bool | None + """Per-transaction override for crypto blockchain analytics screening. When `true`, crypto screening is performed regardless of the application default. When `false`, crypto screening is skipped even if enabled in the application settings. When omitted or `null`, the application-level default configured in the Console is used. Crypto screening requires `transaction_details.direction`, `currency_kind: "crypto"`, a chain/currency, and the wallet address selected by the direction: inbound pre-transfer screening uses `counterparty.payment_method.account_id`, inbound transaction-hash screening uses `subject.payment_method.account_id`, and outbound screening uses `counterparty.payment_method.account_id`. When `transaction_details.payment_reference_id` contains a blockchain transaction hash, Didit performs transaction screening and returns transaction summary enrichment when available.""" diff --git a/src/didit/types/transactions/create_sdk_token_params.py b/src/didit/types/transactions/create_sdk_token_params.py new file mode 100644 index 0000000..708e007 --- /dev/null +++ b/src/didit/types/transactions/create_sdk_token_params.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TransactionsCreateSdkTokenParams"] + + +class TransactionsCreateSdkTokenParams(TypedDict, total=False): + vendor_data: Required[str] + """Your internal identifier for the end user this token is scoped to. Enforced as the subject identity of every transaction submitted with the token.""" + + ttl_seconds: int + """Token lifetime in seconds. Defaults to 900 (15 minutes); maximum 86400 (24 hours).""" + + max_uses: int | None + """Maximum number of successful submissions allowed with this token. Omit or null for unlimited uses within the TTL.""" diff --git a/src/didit/types/transactions/create_sdk_token_response.py b/src/didit/types/transactions/create_sdk_token_response.py new file mode 100644 index 0000000..dc1390b --- /dev/null +++ b/src/didit/types/transactions/create_sdk_token_response.py @@ -0,0 +1,16 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["TransactionsCreateSdkTokenResponse"] + + +class TransactionsCreateSdkTokenResponse(BaseModel): + sdk_token: str | None = None + """The scoped transaction token to pass to the SDK.""" + + expires_at: _dt.datetime | None = None + """When the token stops being accepted.""" diff --git a/src/didit/types/transactions/list_networks_response.py b/src/didit/types/transactions/list_networks_response.py new file mode 100644 index 0000000..667826f --- /dev/null +++ b/src/didit/types/transactions/list_networks_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.entity_network_membership import EntityNetworkMembership + +__all__ = ["TransactionsListNetworksResponse"] + + +class TransactionsListNetworksResponse(BaseModel): + results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/transactions/list_party_networks_response.py b/src/didit/types/transactions/list_party_networks_response.py new file mode 100644 index 0000000..735ea96 --- /dev/null +++ b/src/didit/types/transactions/list_party_networks_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.entity_network_membership import EntityNetworkMembership + +__all__ = ["TransactionsListPartyNetworksResponse"] + + +class TransactionsListPartyNetworksResponse(BaseModel): + results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/transactions/list_response.py b/src/didit/types/transactions/list_response.py new file mode 100644 index 0000000..57970c6 --- /dev/null +++ b/src/didit/types/transactions/list_response.py @@ -0,0 +1,122 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "TransactionsListResponse", + "TransactionsListResponseResults", + "TransactionsListResponseResultsTags", +] + + +class TransactionsListResponseResultsTags(BaseModel): + uuid: str | None = None + + name: str | None = None + + color: str | None = None + + description: str | None = None + + source: Literal["didit", "custom"] | str | None = None + """Whether the tag is a Didit preset or a custom tag.""" + + +class TransactionsListResponseResults(BaseModel): + uuid: str | None = None + """Stable Didit transaction identifier. Use as `{transaction_id}` in detail/notes/related calls.""" + + transaction_number: int | None = None + """Application-scoped sequential transaction number shown in Console (e.g. `4123`).""" + + txn_id: str | None = None + """Customer-provided identifier from the create call (max 128 chars). Unique per application.""" + + transaction_type: str | None = None + """Top-level category as stored: `finance`, `kyc`, `travelRule`, `userPlatformEvent`, `gamblingBet`, `gamblingLimitChange`, `gamblingBonusChange`, `auditTrailEvent`. Note multi-word values are echoed back in camelCase even when submitted in snake_case.""" + + action_type: str | None = None + """Sub-type within the category (e.g. `deposit`, `withdrawal`, `transfer`).""" + + status: Literal["APPROVED", "IN_REVIEW", "DECLINED", "AWAITING_USER"] | str | None = None + """Current monitoring verdict.""" + + direction: Literal["INBOUND", "OUTBOUND"] | str | None = None + """Direction relative to the subject. Stored uppercase regardless of the casing submitted.""" + + amount: str | None = None + """Transaction amount as a decimal string with trailing zeros stripped (e.g. `"1500"`, `"0.5"`, `"0.123456789012345678"`). Up to 18 decimal places.""" + + currency: str | None = None + """Currency code of `amount`. Any ISO 4217 alphabetic code is accepted (e.g. `EUR`, `USD`, `NGN`), as is a crypto asset symbol (e.g. `BTC`). Prefer a currently-circulating code. Not every accepted code has an exchange rate: when none is available for the code (for example the ISO accounting unit `XSU`, or a crypto symbol Didit does not price), automatic conversion fails and `preferred_currency_amount` is returned null unless the request supplied a pre-converted `amount_in_default_currency`.""" + + preferred_currency_amount: str | None = None + """`amount` converted to the application's preferred currency, when available.""" + + preferred_currency_code: str | None = None + """Preferred currency code used for `preferred_currency_amount` - the application's base currency, any currently-circulating ISO 4217 code. A code with no available exchange rate leaves `preferred_currency_amount` null unless the request supplied a pre-converted `amount_in_default_currency`.""" + + payment_details: str | None = None + """Free-text payment reference or memo from the submission.""" + + score: int | None = None + """Risk score (typically 0–100). Higher = riskier.""" + + severity: Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str | None = None + """Categorical risk severity set by rules/providers (`UNKNOWN`, `LOW`, `MEDIUM`, `HIGH`, `CRITICAL`). `null` until something sets it.""" + + subject_name: str | None = None + """Full name of the `APPLICANT` party.""" + + subject_vendor_data: str | None = None + """Your internal user/business identifier for the applicant.""" + + subject_country: str | None = None + """Country code of the applicant, from the submitted `address.country` (typically ISO 3166-1 alpha-3).""" + + counterparty_name: str | None = None + """Full name of the `COUNTERPARTY` party, if any.""" + + counterparty_vendor_data: str | None = None + """Your internal identifier for the counterparty.""" + + counterparty_country: str | None = None + """Country code of the counterparty, from the submitted `address.country`.""" + + tags: list[TransactionsListResponseResultsTags] | None = None + """Tags attached to the transaction (manually or by rules).""" + + txn_date: _dt.datetime | None = None + """When the transaction occurred (from `transaction_at` at create time).""" + + created_at: _dt.datetime | None = None + """When Didit recorded the submission.""" + + decision_reason_code: str | None = None + """Machine-readable reason for the current `status` (e.g. `WALLET_HIGH_RISK`).""" + + decision_reason_label: str | None = None + """Human-readable label for `decision_reason_code`.""" + + assigned_to_name: str | None = None + """Display name of the Console reviewer assigned to this transaction.""" + + +class TransactionsListResponse(BaseModel): + count: int | None = None + """Capped count of matching transactions (exact up to 100, capped thereafter). Use `total_transactions` for the true count when paginating.""" + + next: str | None = None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """Absolute URL of the previous page, or `null` on the first page.""" + + total_transactions: int | None = None + """Exact total count of (non-deleted) transactions for the application. Present on paginated responses.""" + + results: list[TransactionsListResponseResults] | None = None diff --git a/src/didit/types/transactions/rules/__init__.py b/src/didit/types/transactions/rules/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/transactions/rules/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/transactions/rules/backtest_params.py b/src/didit/types/transactions/rules/backtest_params.py new file mode 100644 index 0000000..05cd5bf --- /dev/null +++ b/src/didit/types/transactions/rules/backtest_params.py @@ -0,0 +1,27 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit.types.components.transaction_rule_aggregation import TransactionRuleAggregationParam +from didit.types.components.transaction_rule_condition import TransactionRuleConditionParam +from didit.types.components.transaction_rule_scope import TransactionRuleScopeParam + +__all__ = ["TransactionsRulesBacktestParams"] + + +class TransactionsRulesBacktestParams(TypedDict, total=False): + """Hypothetical rule configuration to evaluate against recent transactions. Read-only and free: nothing is created or modified. Send the same conditions, aggregation, evaluation_mode AND scope as the rule you intend to activate. Omitted conditions, aggregation and scope add no constraints of their own; dropping scope can include directions or transaction types the saved rule skips. Omitted evaluation_mode defaults to ALL (which can reduce matches for an ANY rule), and omitted period_days defaults to 90 days, not unlimited history.""" + + conditions: Sequence[TransactionRuleConditionParam] + + aggregation: Sequence[TransactionRuleAggregationParam] + + evaluation_mode: Literal["ALL", "ANY"] | str + + scope: TransactionRuleScopeParam + """Which transactions the rule applies to. Only these three keys are accepted.""" + + period_days: int diff --git a/src/didit/types/transactions/rules/create_params.py b/src/didit/types/transactions/rules/create_params.py new file mode 100644 index 0000000..f913e31 --- /dev/null +++ b/src/didit/types/transactions/rules/create_params.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.transaction_rule_action import TransactionRuleActionParam +from didit.types.components.transaction_rule_aggregation import TransactionRuleAggregationParam +from didit.types.components.transaction_rule_condition import TransactionRuleConditionParam +from didit.types.components.transaction_rule_scope import TransactionRuleScopeParam + +__all__ = ["TransactionsRulesCreateParams"] + + +class TransactionsRulesCreateParams(TypedDict, total=False): + """Creates a CUSTOM rule. Rules in ACTIVE mode take effect immediately for new transactions - create in TEST mode and backtest first.""" + + title: Required[str] + """Rule name. Unique per application among non-deleted rules; duplicates return 400.""" + + description: str | None + + category: Required[str] + """Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce.""" + + mode: Required[Literal["ACTIVE", "DISABLED", "TEST"] | str] + """ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped.""" + + evaluation_mode: Literal["ALL", "ANY"] | str + """Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index.""" + + scope: TransactionRuleScopeParam + """Which transactions the rule applies to. Only these three keys are accepted.""" + + conditions: Sequence[TransactionRuleConditionParam] + + aggregation: Sequence[TransactionRuleAggregationParam] + + actions: Sequence[TransactionRuleActionParam] + + metadata: Mapping[str, object] + """Free-form metadata stored on the rule.""" diff --git a/src/didit/types/transactions/rules/install_library_params.py b/src/didit/types/transactions/rules/install_library_params.py new file mode 100644 index 0000000..430a47e --- /dev/null +++ b/src/didit/types/transactions/rules/install_library_params.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["TransactionsRulesInstallLibraryParams"] + + +class TransactionsRulesInstallLibraryParams(TypedDict, total=False): + """Provide library_keys OR bundle.""" + + library_keys: Sequence[str] + """Specific presets to install or uninstall.""" + + bundle: str + """Install or uninstall every preset in this bundle instead.""" diff --git a/src/didit/types/transactions/rules/list_library_response.py b/src/didit/types/transactions/rules/list_library_response.py new file mode 100644 index 0000000..f65fcc4 --- /dev/null +++ b/src/didit/types/transactions/rules/list_library_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.transaction_rule_library_entry import TransactionRuleLibraryEntry + +__all__ = ["TransactionsRulesListLibraryResponse"] + + +class TransactionsRulesListLibraryResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[TransactionRuleLibraryEntry] | None = None diff --git a/src/didit/types/transactions/rules/list_response.py b/src/didit/types/transactions/rules/list_response.py new file mode 100644 index 0000000..2d31f29 --- /dev/null +++ b/src/didit/types/transactions/rules/list_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.transaction_rule_detail import TransactionRuleDetail + +__all__ = ["TransactionsRulesListResponse"] + + +class TransactionsRulesListResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[TransactionRuleDetail] | None = None diff --git a/src/didit/types/transactions/rules/uninstall_library_params.py b/src/didit/types/transactions/rules/uninstall_library_params.py new file mode 100644 index 0000000..08b61b1 --- /dev/null +++ b/src/didit/types/transactions/rules/uninstall_library_params.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["TransactionsRulesUninstallLibraryParams"] + + +class TransactionsRulesUninstallLibraryParams(TypedDict, total=False): + """Provide library_keys OR bundle.""" + + library_keys: Sequence[str] + """Specific presets to install or uninstall.""" + + bundle: str + """Install or uninstall every preset in this bundle instead.""" diff --git a/src/didit/types/transactions/rules/update_params.py b/src/didit/types/transactions/rules/update_params.py new file mode 100644 index 0000000..7cf0f9b --- /dev/null +++ b/src/didit/types/transactions/rules/update_params.py @@ -0,0 +1,43 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping, Sequence + +from typing_extensions import TypedDict + +from didit.types.components.transaction_rule_action import TransactionRuleActionParam +from didit.types.components.transaction_rule_aggregation import TransactionRuleAggregationParam +from didit.types.components.transaction_rule_condition import TransactionRuleConditionParam +from didit.types.components.transaction_rule_scope import TransactionRuleScopeParam + +__all__ = ["TransactionsRulesUpdateParams"] + + +class TransactionsRulesUpdateParams(TypedDict, total=False): + """Partial update. Custom rules accept every field; library (PRESET) rules accept only mode - anything else returns 400.""" + + title: str + """Rule name. Unique per application among non-deleted rules; duplicates return 400.""" + + description: str | None + + category: str + """Risk theme used for grouping and reporting, e.g. finance, aml_ctf, anomaly_detection, fatf, device_intelligence, crypto_monitoring, travel_rule, responsible_gaming, e_commerce.""" + + mode: Literal["ACTIVE", "DISABLED", "TEST"] | str + """ACTIVE evaluates live. TEST evaluates and records rule runs but never touches score or status - the recommended starting mode. DISABLED is skipped.""" + + evaluation_mode: Literal["ALL", "ANY"] | str + """Whether every condition must match (ALL) or at least one (ANY). Ignored when conditions use group_index.""" + + scope: TransactionRuleScopeParam + """Which transactions the rule applies to. Only these three keys are accepted.""" + + conditions: Sequence[TransactionRuleConditionParam] + + aggregation: Sequence[TransactionRuleAggregationParam] + + actions: Sequence[TransactionRuleActionParam] + + metadata: Mapping[str, object] + """Free-form metadata stored on the rule.""" diff --git a/src/didit/types/transactions/travel_rule/__init__.py b/src/didit/types/transactions/travel_rule/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/transactions/travel_rule/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/transactions/travel_rule/confirm_ownership_params.py b/src/didit/types/transactions/travel_rule/confirm_ownership_params.py new file mode 100644 index 0000000..348f363 --- /dev/null +++ b/src/didit/types/transactions/travel_rule/confirm_ownership_params.py @@ -0,0 +1,10 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TransactionsTravelRuleConfirmOwnershipParams"] + + +class TransactionsTravelRuleConfirmOwnershipParams(TypedDict, total=False): + confirmed: Required[bool] diff --git a/src/didit/types/transactions/travel_rule/update_transfer_params.py b/src/didit/types/transactions/travel_rule/update_transfer_params.py new file mode 100644 index 0000000..8349ea5 --- /dev/null +++ b/src/didit/types/transactions/travel_rule/update_transfer_params.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +__all__ = ["TransactionsTravelRuleUpdateTransferParams"] + + +class TransactionsTravelRuleUpdateTransferParams(TypedDict, total=False): + """Provide exactly one of: payment_txn_id (finish), action=cancel, or action=resend.""" + + payment_txn_id: str + """The on-chain transaction hash. Requires the transfer to be an outbound transfer in COMPLETED status.""" + + action: Literal["cancel", "resend"] | str + """"cancel" cancels a non-terminal transfer. "resend" re-runs routing for an outbound transfer stuck in COUNTERPARTY_VASP_NOT_FOUND, COUNTERPARTY_VASP_NOT_REACHABLE, or NOT_ENOUGH_COUNTERPARTY_DATA.""" diff --git a/src/didit/types/travel_rule/__init__.py b/src/didit/types/travel_rule/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/travel_rule/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/travel_rule/create_widget_session_params.py b/src/didit/types/travel_rule/create_widget_session_params.py new file mode 100644 index 0000000..7dda729 --- /dev/null +++ b/src/didit/types/travel_rule/create_widget_session_params.py @@ -0,0 +1,32 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleCreateWidgetSessionParams"] + + +class TravelRuleCreateWidgetSessionParams(TypedDict, total=False): + wallet_address: Required[str] + """The wallet address to verify.""" + + chain: Required[str] + """Chain identifier for the address, e.g. "ethereum", "bitcoin", "solana".""" + + holder_name: str + """Name of the wallet holder, used for the beneficiary name match.""" + + vendor_data: str + """Your internal identifier for the holder, carried onto the created address-book entry.""" + + transaction_id: str + """Optional. Didit transaction UUID of a Travel Rule transfer to advance when the proof verifies.""" + + satoshi_deposit_address: str + """Optional. A deposit address you control; supplying it enables the SATOSHI_TEST proof method.""" + + callback_url: str + """Optional. Where the widget returns the customer after completion.""" + + expires_in_minutes: int + """Optional. Lifetime of the minted link, in minutes. Automatically extended to cover the Satoshi test window when SATOSHI_TEST is offered.""" diff --git a/src/didit/types/travel_rule/list_vasps_response.py b/src/didit/types/travel_rule/list_vasps_response.py new file mode 100644 index 0000000..ed9fd1c --- /dev/null +++ b/src/didit/types/travel_rule/list_vasps_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.vasp_directory_entry import VaspDirectoryEntry + +__all__ = ["TravelRuleListVaspsResponse"] + + +class TravelRuleListVaspsResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[VaspDirectoryEntry] | None = None diff --git a/src/didit/types/travel_rule/register_inbound_params.py b/src/didit/types/travel_rule/register_inbound_params.py new file mode 100644 index 0000000..f165a71 --- /dev/null +++ b/src/didit/types/travel_rule/register_inbound_params.py @@ -0,0 +1,45 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Mapping + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleRegisterInboundParams", "TravelRuleRegisterInboundParamsOriginatingVasp"] + + +class TravelRuleRegisterInboundParamsOriginatingVasp(TypedDict, total=False): + """Optional description of the sending VASP.""" + + name: str + + lei: str + + travel_address: str + """Travel address that lets Didit reach the sender on the TRP rail.""" + + +class TravelRuleRegisterInboundParams(TypedDict, total=False): + chain: Required[str] + """Chain the deposit settled on, e.g. "ethereum".""" + + txid: Required[str] + """On-chain transaction hash. Deduped together with wallet_address.""" + + wallet_address: Required[str] + """The destination wallet you received the deposit on.""" + + amount: Required[str] + """Deposit amount as a decimal string.""" + + currency: Required[str] + """Deposit asset, e.g. "USDC".""" + + originator_data: Mapping[str, object] + """IVMS-101 originator payload from the sending VASP, if known.""" + + beneficiary_data: Mapping[str, object] + """IVMS-101 beneficiary payload (your customer).""" + + originating_vasp: TravelRuleRegisterInboundParamsOriginatingVasp + """Optional description of the sending VASP.""" diff --git a/src/didit/types/travel_rule/update_settings_params.py b/src/didit/types/travel_rule/update_settings_params.py new file mode 100644 index 0000000..8866717 --- /dev/null +++ b/src/didit/types/travel_rule/update_settings_params.py @@ -0,0 +1,47 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +__all__ = ["TravelRuleUpdateSettingsParams"] + + +class TravelRuleUpdateSettingsParams(TypedDict, total=False): + """All fields optional; PUT applies a partial update.""" + + is_enabled: bool + + legal_name: str + + lei: str + + jurisdiction: str + + name_matching_strictness: Literal["NONE", "STRICT", "DEFAULT", "FUZZY"] | str + + confirmation_timeout_hours: int + + timeout_outcome: Literal["HOLD", "REJECT", "PROCEED"] | str + + compliance_email: str + + is_discoverable: bool + + threshold_amount: str + """Decimal string. Minimum transfer amount that triggers a managed exchange.""" + + inbound_auto_accept: bool + + allow_self_declaration: bool + + allow_screenshot_proof: bool + """When true, the wallet-ownership widget offers screenshot upload as an ownership evidence method. Default true.""" + + auto_wallet_verification: bool + + vasp_attribution_enabled: bool + + disabled_networks: Sequence[Literal["GTR", "TRUST", "VERIFYVASP", "SYGNA"] | str] + """Networks opted out of platform membership. Does not disable your connected own memberships or provision unavailable networks.""" diff --git a/src/didit/types/travel_rule/wallet_addresses/__init__.py b/src/didit/types/travel_rule/wallet_addresses/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/travel_rule/wallet_addresses/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/travel_rule/wallet_addresses/create_params.py b/src/didit/types/travel_rule/wallet_addresses/create_params.py new file mode 100644 index 0000000..9a5495e --- /dev/null +++ b/src/didit/types/travel_rule/wallet_addresses/create_params.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["TravelRuleWalletAddressesCreateParams"] + + +class TravelRuleWalletAddressesCreateParams(TypedDict, total=False): + address: Required[str] + + chain: str + + holder_vendor_data: str + + holder_name: str + + entity_type: str + + self_declared: bool + """Write-only. When true, the entry is created already ownership-verified via a SELF_DECLARATION proof.""" + + travel_address: str + """Optional. The counterparty VASP's travel address for this wallet, enabling TRP routing.""" diff --git a/src/didit/types/travel_rule/wallet_addresses/list_response.py b/src/didit/types/travel_rule/wallet_addresses/list_response.py new file mode 100644 index 0000000..a853c19 --- /dev/null +++ b/src/didit/types/travel_rule/wallet_addresses/list_response.py @@ -0,0 +1,17 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.wallet_address_entry import WalletAddressEntry + +__all__ = ["TravelRuleWalletAddressesListResponse"] + + +class TravelRuleWalletAddressesListResponse(BaseModel): + count: int | None = None + + next: str | None = None + + previous: str | None = None + + results: list[WalletAddressEntry] | None = None diff --git a/src/didit/types/travel_rule/wallet_addresses/update_params.py b/src/didit/types/travel_rule/wallet_addresses/update_params.py new file mode 100644 index 0000000..9b24ce9 --- /dev/null +++ b/src/didit/types/travel_rule/wallet_addresses/update_params.py @@ -0,0 +1,18 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["TravelRuleWalletAddressesUpdateParams"] + + +class TravelRuleWalletAddressesUpdateParams(TypedDict, total=False): + """PATCH is a partial update; the response echoes only holder_name, holder_vendor_data, entity_type, and travel_address.""" + + holder_name: str + + holder_vendor_data: str + + entity_type: str + + travel_address: str diff --git a/src/didit/types/users/__init__.py b/src/didit/types/users/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/users/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/users/batch_delete_params.py b/src/didit/types/users/batch_delete_params.py new file mode 100644 index 0000000..e657d28 --- /dev/null +++ b/src/didit/types/users/batch_delete_params.py @@ -0,0 +1,21 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import TypedDict + +__all__ = ["UsersBatchDeleteParams"] + + +class UsersBatchDeleteParams(TypedDict, total=False): + """Provide `vendor_data_list`, `didit_internal_id_list`, or `delete_all: true`. The two lists can be combined and are OR'd. When `delete_all` is `true` it takes precedence and both lists are ignored.""" + + vendor_data_list: Sequence[str] + """Your own identifiers to delete, matched exactly as sent.""" + + didit_internal_id_list: Sequence[str] + """Didit internal user IDs to delete - the `didit_internal_id` returned by the create and get endpoints.""" + + delete_all: bool + """If `true`, deletes every user in the application.""" diff --git a/src/didit/types/users/batch_delete_response.py b/src/didit/types/users/batch_delete_response.py new file mode 100644 index 0000000..e037ccf --- /dev/null +++ b/src/didit/types/users/batch_delete_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel + +__all__ = ["UsersBatchDeleteResponse"] + + +class UsersBatchDeleteResponse(BaseModel): + deleted: int | None = None + """Number of users deleted (excludes vendor_data values that didn't match anything).""" diff --git a/src/didit/types/users/create_params.py b/src/didit/types/users/create_params.py new file mode 100644 index 0000000..6b58b13 --- /dev/null +++ b/src/didit/types/users/create_params.py @@ -0,0 +1,38 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["UsersCreateParams"] + + +class UsersCreateParams(TypedDict, total=False): + vendor_data: Required[str] + """Your unique identifier for this user (free-form string, NOT a UUID). Must be unique among non-deleted users for the application; matched exactly as sent.""" + + full_name: str | None + """Full legal name of the user.""" + + display_name: str | None + """Friendly display name shown in the console (takes precedence over `full_name` for UI display).""" + + date_of_birth: str | _dt.date | None + """Date of birth in `YYYY-MM-DD` format.""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str + """Initial lifecycle status. Defaults to `ACTIVE`.""" + + metadata: Mapping[str, object] | None + """Arbitrary JSON object you attach to the user. Defaults to `{}`.""" + + approved_emails: Sequence[str] + """Pre-trusted email addresses for this user, e.g. `["john@example.com"]`.""" + + approved_phones: Sequence[str] + """Pre-trusted phone numbers for this user, e.g. `["+14155551234"]`.""" + + issuing_states: Sequence[str] + """Pre-recorded issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`.""" diff --git a/src/didit/types/users/list_networks_response.py b/src/didit/types/users/list_networks_response.py new file mode 100644 index 0000000..c7d2954 --- /dev/null +++ b/src/didit/types/users/list_networks_response.py @@ -0,0 +1,11 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.entity_network_membership import EntityNetworkMembership + +__all__ = ["UsersListNetworksResponse"] + + +class UsersListNetworksResponse(BaseModel): + results: list[EntityNetworkMembership] | None = None diff --git a/src/didit/types/users/list_response.py b/src/didit/types/users/list_response.py new file mode 100644 index 0000000..44a2a45 --- /dev/null +++ b/src/didit/types/users/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.user_list_item import UserListItem + +__all__ = ["UsersListResponse"] + + +class UsersListResponse(BaseModel): + count: int | None = None + """Number of matching rows - exact up to 100, capped at 100 beyond that for performance. Do not use it to detect the last page; rely on `next` being `null` instead.""" + + next: str | None = None + """Absolute URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """Absolute URL of the previous page, or `null` on the first page.""" + + results: list[UserListItem] | None = None diff --git a/src/didit/types/users/retrieve_ongoing_monitoring_job_response.py b/src/didit/types/users/retrieve_ongoing_monitoring_job_response.py new file mode 100644 index 0000000..8b5d4b3 --- /dev/null +++ b/src/didit/types/users/retrieve_ongoing_monitoring_job_response.py @@ -0,0 +1,88 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "UsersRetrieveOngoingMonitoringJobResponse", + "UsersRetrieveOngoingMonitoringJobResponseEstimate", + "UsersRetrieveOngoingMonitoringJobResponseOutcomes", +] + + +class UsersRetrieveOngoingMonitoringJobResponseEstimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class UsersRetrieveOngoingMonitoringJobResponseOutcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class UsersRetrieveOngoingMonitoringJobResponse(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: UsersRetrieveOngoingMonitoringJobResponseOutcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: UsersRetrieveOngoingMonitoringJobResponseEstimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/users/set_aml_monitoring_params.py b/src/didit/types/users/set_aml_monitoring_params.py new file mode 100644 index 0000000..9ee1733 --- /dev/null +++ b/src/didit/types/users/set_aml_monitoring_params.py @@ -0,0 +1,24 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["UsersSetAmlMonitoringParams"] + + +class UsersSetAmlMonitoringParams(TypedDict, total=False): + """Select users with `vendor_data_list`, `didit_internal_id_list`, or both (the two lists are OR'd). At most 1000 identifiers per request across both lists.""" + + is_enabled: Required[bool] + """true to enable monitoring, false to disable it.""" + + estimate_only: bool + """When true, returns the cost estimate and performs no writes - no screening, no billing, no job.""" + + vendor_data_list: Sequence[str] + """Your own identifiers of the users to toggle, matched exactly as sent.""" + + didit_internal_id_list: Sequence[str] + """Didit internal ids of the users to toggle - the `didit_internal_id` returned by the create, get and list endpoints.""" diff --git a/src/didit/types/users/set_aml_monitoring_response.py b/src/didit/types/users/set_aml_monitoring_response.py new file mode 100644 index 0000000..f567974 --- /dev/null +++ b/src/didit/types/users/set_aml_monitoring_response.py @@ -0,0 +1,299 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "UsersSetAmlMonitoringResponse202", + "UsersSetAmlMonitoringResponse202Estimate", + "UsersSetAmlMonitoringResponse202Outcomes", + "UsersSetAmlMonitoringResponse202Results", + "UsersSetAmlMonitoringResponseVariant1", + "UsersSetAmlMonitoringResponseVariant2", + "UsersSetAmlMonitoringResponseVariant2Estimate", + "UsersSetAmlMonitoringResponseVariant2Outcomes", + "UsersSetAmlMonitoringResponseVariant2Results", +] + + +class UsersSetAmlMonitoringResponseVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int + """Profiles matched by the selection.""" + + already_enabled: int + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + unmatched: list[str] + """Identifiers of the request that matched no profile in this application, exactly as sent.""" + + +class UsersSetAmlMonitoringResponseVariant2Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int | None = None + """Profiles matched by the selection.""" + + already_enabled: int | None = None + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int | None = None + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int | None = None + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int | None = None + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int | None = None + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float | None = None + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float | None = None + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool | None = None + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + +class UsersSetAmlMonitoringResponseVariant2Outcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class UsersSetAmlMonitoringResponseVariant2Results(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class UsersSetAmlMonitoringResponseVariant2(BaseModel): + job_id: str + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str + + is_enabled: bool + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str + """Which profile family the job toggles.""" + + total: int + """Profiles the job processes.""" + + processed: int + """Profiles processed so far.""" + + outcomes: UsersSetAmlMonitoringResponseVariant2Outcomes + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[UsersSetAmlMonitoringResponseVariant2Results] + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" + + error: str | None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime + + completed_at: _dt.datetime | None + + estimate: UsersSetAmlMonitoringResponseVariant2Estimate + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + unmatched: list[str] + """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" + + +class UsersSetAmlMonitoringResponse202Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + users_total: int | None = None + """Profiles matched by the selection.""" + + already_enabled: int | None = None + """Profiles already monitored (nothing to do when enabling; these are what disabling turns off).""" + + with_existing_aml: int | None = None + """Profiles whose existing AML screening will be switched to monitored (no new screening).""" + + needing_screening: int | None = None + """Profiles without any AML screening that will be screened first (billed as a regular AML screening).""" + + skipped_no_identity_data: int | None = None + """Profiles that cannot be screened because identity data is missing.""" + + skipped_blocked: int | None = None + """Blocked users that will be skipped (always 0 for businesses).""" + + estimated_screening_cost_usd: float | None = None + """One-time cost of the new screenings.""" + + estimated_annual_monitoring_cost_usd: float | None = None + """Yearly monitoring cost of the profiles that will become monitored.""" + + balance_sufficient: bool | None = None + """Whether the organization balance covers the screening plus the first year of monitoring.""" + + +class UsersSetAmlMonitoringResponse202Outcomes(BaseModel): + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled on it.""" + + already_enabled: int | None = None + """Monitoring was already active; nothing changed and nothing was billed.""" + + disabled: int | None = None + """Monitoring turned off across every AML screening of the profile.""" + + skipped_no_identity_data: int | None = None + """Skipped: the profile lacks the full name, date of birth and country (users) or a name (businesses) needed to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application, no screening is created.""" + + failed_provider: int | None = None + """The screening could not be completed; retry later.""" + + failed_insufficient_balance: int | None = None + """Balance ran out while the job was running.""" + + +class UsersSetAmlMonitoringResponse202Results(BaseModel): + didit_internal_id: str | None = None + """Didit internal id of the profile.""" + + vendor_data: str | None = None + """Your own identifier for the profile.""" + + outcome: ( + Literal[ + "enabled", + "screened_and_enabled", + "already_enabled", + "disabled", + "skipped_no_identity_data", + "skipped_blocked", + "skipped_sandbox", + "failed_provider", + "failed_insufficient_balance", + ] + | str + | None + ) = None + + +class UsersSetAmlMonitoringResponse202(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + """true when the job enables monitoring, false when it disables it.""" + + target_type: Literal["user", "business"] | str | None = None + """Which profile family the job toggles.""" + + total: int | None = None + """Profiles the job processes.""" + + processed: int | None = None + """Profiles processed so far.""" + + outcomes: UsersSetAmlMonitoringResponse202Outcomes | None = None + """Per-record outcome counters. Every selected profile resolves to exactly one counter; nothing is silently skipped.""" + + results: list[UsersSetAmlMonitoringResponse202Results] | None = None + """One entry per processed profile, in processing order. Grows while the job runs; complete once status is COMPLETED. Recorded for jobs started through the API.""" + + error: str | None = None + """Generic failure message when status is FAILED; the detailed cause is kept on the platform side.""" + + created_at: _dt.datetime | None = None + + completed_at: _dt.datetime | None = None + + estimate: UsersSetAmlMonitoringResponse202Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (nothing is written).""" + + unmatched: list[str] | None = None + """Identifiers of the request that matched no profile in this application, exactly as sent. They are not part of the job.""" diff --git a/src/didit/types/users/set_ongoing_monitoring_params.py b/src/didit/types/users/set_ongoing_monitoring_params.py new file mode 100644 index 0000000..9fa5afc --- /dev/null +++ b/src/didit/types/users/set_ongoing_monitoring_params.py @@ -0,0 +1,33 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Sequence + +from typing_extensions import Required, TypedDict + +__all__ = ["UsersSetOngoingMonitoringParams"] + + +class UsersSetOngoingMonitoringParams(TypedDict, total=False): + is_enabled: Required[bool] + """true to enable monitoring, false to disable it.""" + + estimate_only: bool + """When true, returns the cost estimate and performs no writes.""" + + update_all: bool + """Apply to every entity of the application (scoped by the filter_* params when present).""" + + filter_search: str + + filter_status: Sequence[str] + + filter_country: str + + filter_date_from: str | _dt.date + + filter_date_to: str | _dt.date + + didit_internal_id_list: Sequence[str] + """Entities to toggle. Required unless update_all is true.""" diff --git a/src/didit/types/users/set_ongoing_monitoring_response.py b/src/didit/types/users/set_ongoing_monitoring_response.py new file mode 100644 index 0000000..e61816b --- /dev/null +++ b/src/didit/types/users/set_ongoing_monitoring_response.py @@ -0,0 +1,193 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from didit._models import BaseModel + +__all__ = [ + "UsersSetOngoingMonitoringResponse202", + "UsersSetOngoingMonitoringResponse202Estimate", + "UsersSetOngoingMonitoringResponse202Outcomes", + "UsersSetOngoingMonitoringResponseVariant1", + "UsersSetOngoingMonitoringResponseVariant2", + "UsersSetOngoingMonitoringResponseVariant2Estimate", + "UsersSetOngoingMonitoringResponseVariant2Outcomes", +] + + +class UsersSetOngoingMonitoringResponseVariant1(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class UsersSetOngoingMonitoringResponseVariant2Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class UsersSetOngoingMonitoringResponseVariant2Outcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class UsersSetOngoingMonitoringResponseVariant2(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: UsersSetOngoingMonitoringResponseVariant2Outcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: UsersSetOngoingMonitoringResponseVariant2Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + +class UsersSetOngoingMonitoringResponse202Estimate(BaseModel): + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" + + users_total: int | None = None + """Entities in the selection.""" + + already_enabled: int | None = None + + with_existing_aml: int | None = None + """Entities whose existing AML screening will be switched to monitored.""" + + needing_screening: int | None = None + """Entities without any AML screening that will be screened first (billed).""" + + skipped_no_identity_data: int | None = None + + skipped_blocked: int | None = None + + estimated_screening_cost_usd: float | None = None + + estimated_annual_monitoring_cost_usd: float | None = None + + balance_sufficient: bool | None = None + + +class UsersSetOngoingMonitoringResponse202Outcomes(BaseModel): + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + enabled: int | None = None + """Monitoring enabled on an existing AML screening.""" + + screened_and_enabled: int | None = None + """A new AML screening was created (billed) and monitoring enabled.""" + + already_enabled: int | None = None + """Monitoring was already active (legacy duplicate rows are consolidated).""" + + disabled: int | None = None + """Monitoring disabled on every AML row of the entity.""" + + skipped_no_identity_data: int | None = None + """Skipped: no full name + date of birth + country available to screen.""" + + skipped_blocked: int | None = None + """Skipped: the user is blocked.""" + + skipped_sandbox: int | None = None + """Skipped: sandbox application.""" + + failed_provider: int | None = None + """The screening provider call failed.""" + + failed_insufficient_balance: int | None = None + """Balance ran out mid-job.""" + + +class UsersSetOngoingMonitoringResponse202(BaseModel): + job_id: str | None = None + + status: Literal["PENDING", "PROCESSING", "COMPLETED", "FAILED"] | str | None = None + + is_enabled: bool | None = None + + total: int | None = None + + processed: int | None = None + + outcomes: UsersSetOngoingMonitoringResponse202Outcomes | None = None + """Per-entity outcome counters. Nothing is silently skipped: every requested entity resolves to exactly one counter.""" + + error: str | None = None + + estimate: UsersSetOngoingMonitoringResponse202Estimate | None = None + """Cost preview. Returned directly when estimate_only is true (no writes are performed).""" diff --git a/src/didit/types/users/update_params.py b/src/didit/types/users/update_params.py new file mode 100644 index 0000000..f9e3ca5 --- /dev/null +++ b/src/didit/types/users/update_params.py @@ -0,0 +1,35 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal, Mapping, Sequence + +from typing_extensions import TypedDict + +__all__ = ["UsersUpdateParams"] + + +class UsersUpdateParams(TypedDict, total=False): + full_name: str | None + """Full name of the user""" + + display_name: str | None + """Custom display name for this user""" + + date_of_birth: str | _dt.date | None + """Date of birth in YYYY-MM-DD format""" + + status: Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str + """User lifecycle status. Changing it via this generic PATCH does NOT sync the system blocklist or record a status-change activity; prefer the update-status endpoint.""" + + metadata: Mapping[str, object] | None + """Arbitrary JSON object you attach to the user. **Fully replaces** the existing metadata on update - merge client-side if you only want to add keys.""" + + approved_emails: Sequence[str] + """Verified email addresses for this user, e.g. `["john@example.com"]`. Fully replaces the stored list.""" + + approved_phones: Sequence[str] + """Verified phone numbers for this user. Fully replaces the stored list.""" + + issuing_states: Sequence[str] + """Issuing countries (ISO 3166-1 alpha-3), e.g. `["USA"]`. Fully replaces the stored list.""" diff --git a/src/didit/types/users/update_status_params.py b/src/didit/types/users/update_status_params.py new file mode 100644 index 0000000..2b0f161 --- /dev/null +++ b/src/didit/types/users/update_status_params.py @@ -0,0 +1,13 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["UsersUpdateStatusParams"] + + +class UsersUpdateStatusParams(TypedDict, total=False): + status: Required[Literal["ACTIVE", "FLAGGED", "BLOCKED"] | str] + """New lifecycle status. `BLOCKED` also adds the `vendor_data` to the system blocklist.""" diff --git a/src/didit/types/users/upload_face_params.py b/src/didit/types/users/upload_face_params.py new file mode 100644 index 0000000..d626b4c --- /dev/null +++ b/src/didit/types/users/upload_face_params.py @@ -0,0 +1,14 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing_extensions import Required, TypedDict + +__all__ = ["UsersUploadFaceParams"] + + +class UsersUploadFaceParams(TypedDict, total=False): + image: Required[str] + """Raw base64-encoded JPG or PNG face image. Do not include a `data:image/...;base64,` prefix. Max decoded size: 2 MB.""" + + comment: str + """Optional operator-visible note for the uploaded face.""" diff --git a/src/didit/types/users/upload_face_response.py b/src/didit/types/users/upload_face_response.py new file mode 100644 index 0000000..e971481 --- /dev/null +++ b/src/didit/types/users/upload_face_response.py @@ -0,0 +1,25 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt + +from didit._models import BaseModel + +__all__ = ["UsersUploadFaceResponse"] + + +class UsersUploadFaceResponse(BaseModel): + uuid: str + """UUID of the uploaded User face record.""" + + image_url: str | None + """Signed URL for the uploaded face image.""" + + comment: str | None + """Comment supplied when the face was uploaded.""" + + uploaded_by: str | None + """Email or actor identifier of the uploader when available.""" + + created_at: _dt.datetime + """Upload timestamp.""" diff --git a/src/didit/types/wallet_screening/__init__.py b/src/didit/types/wallet_screening/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/wallet_screening/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/wallet_screening/screen_params.py b/src/didit/types/wallet_screening/screen_params.py new file mode 100644 index 0000000..1d27b5f --- /dev/null +++ b/src/didit/types/wallet_screening/screen_params.py @@ -0,0 +1,39 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +__all__ = ["WalletScreeningScreenParams"] + + +class WalletScreeningScreenParams(TypedDict, total=False): + wallet_address: Required[str] + """The crypto address or network identifier to screen. Must match the selected network (e.g. a `0x`-prefixed 40-hex-char address for EVM chains). Alias: `address`.""" + + blockchain: Required[ + Literal[ + "BTC", + "LIGHTNING", + "ETH", + "LTC", + "XRP", + "BCH", + "DOGE", + "TRX", + "SOL", + "MATIC", + "BNB", + "USDT", + "USDC", + "BASE", + "OP", + "ARB", + ] + | str + ] + """Network identifier. Managed screening supports BTC, ETH, LTC, XRP, BCH, DOGE, TRX, SOL, MATIC, BNB, BASE (Base), OP (Optimism), and ARB (Arbitrum One). USDT and USDC are legacy Ethereum aliases, not multi-network searches. Connected-provider coverage varies. LIGHTNING requires a supported connected provider. Celo, Mode and Plume are not currently supported. Alias: `currency`.""" + + direction: Literal["inbound", "outbound", "deposit", "withdrawal"] | str + """Optional. Whether the address is being screened as an inbound (deposit) or outbound (withdrawal) counterparty. Accepts `inbound`, `outbound`, `deposit`, or `withdrawal`. Defaults to a neutral pre-transfer screen when omitted. This is passed to the provider as context only; it does not change the returned `risk_score`, `severity`, or the `source_of_funds`/`destination_of_funds` breakdowns, which are always both returned regardless of `direction`. Changing or omitting `direction` therefore does not require re-screening an address.""" diff --git a/src/didit/types/wallet_screening/screen_pdf_params.py b/src/didit/types/wallet_screening/screen_pdf_params.py new file mode 100644 index 0000000..7038bbb --- /dev/null +++ b/src/didit/types/wallet_screening/screen_pdf_params.py @@ -0,0 +1,283 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Mapping, Sequence + +from typing_extensions import Required, TypedDict + +__all__ = [ + "WalletScreeningScreenPdfParams", + "WalletScreeningScreenPdfParamsCounterpartyConnections", + "WalletScreeningScreenPdfParamsDestinationOfFunds", + "WalletScreeningScreenPdfParamsRiskFactors", + "WalletScreeningScreenPdfParamsSourceOfFunds", +] + + +class WalletScreeningScreenPdfParamsCounterpartyConnections(TypedDict, total=False): + entity_name: str | None + """Attributed entity name.""" + + entity_type: str | None + """Provider entity type.""" + + entity_subtype: str | None + """Provider entity subtype.""" + + risk_level: str + """Lowercase and provider-dependent. For Merkle Science it is the address's own 0-5 risk grade (`no_risk`, `low`, `medium_low`, `medium`, `high`, `critical`), raised to `high` when the entity's category is high risk; it is not a separate grade of the entity.""" + + categories: Sequence[str] + """Normalised risk categories of the entity.""" + + received_usd: float | None + """USD received from the entity, when the provider reports it.""" + + sent_usd: float | None + """USD sent to the entity, when the provider reports it.""" + + received_hops: int | None + """Hops to the entity on the receiving side, when the provider reports them.""" + + sent_hops: int | None + """Hops to the entity on the sending side, when the provider reports them.""" + + percentage: float + """Share of the address's exposure, when the provider reports it; otherwise 0.""" + + is_direct: bool + """Always `true` for Merkle Science, whose connections are attributions rather than counterparties.""" + + country: str | None + """Country of the entity, when known.""" + + provider_entity_id: str | None + """The provider's own identifier for the entity.""" + + +class WalletScreeningScreenPdfParamsDestinationOfFunds(TypedDict, total=False): + category: str + """Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`.""" + + percentage: float + """Share of the direction's total exposure, 0-100.""" + + amount_usd: float | None + """USD value attributed to this entity.""" + + entity_name: str | None + """Name of the attributed entity, for example an exchange, a mixer or a sanctioned group.""" + + entity_type: str | None + """Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`).""" + + entity_subtype: str | None + """More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`).""" + + hops: int | None + """Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`.""" + + description: str | None + """`entity_type / entity_subtype`, as the provider labels the entity.""" + + is_direct: bool + """True when `exposure_type` is `direct`.""" + + exposure_direction: Literal["incoming", "outgoing", "connected"] | str + """Whether the funds came from (`incoming`) or went to (`outgoing`) this entity.""" + + exposure_type: Literal["direct", "indirect"] | str + """`direct` (1 hop) or `indirect` (multi-hop) exposure.""" + + provider_entity_id: str | None + """The provider's own identifier for the entity.""" + + provider_entity_type_id: str | None + """The provider's own identifier for `entity_type`.""" + + provider_entity_subtype_id: str | None + """The provider's own identifier for `entity_subtype`.""" + + country: str | None + """Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it.""" + + raw_amount: float | None + """Amount in the asset's own unit, when the provider reports it.""" + + raw_currency: str | None + """Unit of `raw_amount`, when the provider reports it.""" + + +class WalletScreeningScreenPdfParamsRiskFactors(TypedDict, total=False): + source: Literal["attribution", "exposure"] | str + """`attribution` is what the provider says the address itself is (its own tag or cluster); `exposure` is where its funds actually moved.""" + + category: str + """Normalised risk category, same vocabulary as the exposure breakdown.""" + + label: str + """The provider's own wording for the driver (e.g. `Gambling / Crypto only`), which is what you would also see in the provider's console.""" + + entity_name: str | None + """Attributed entity behind the driver, for example a gambling site or an exchange.""" + + direction: Literal["incoming", "outgoing", "connected", "unknown", "attribution"] | str + """Direction of the fund flow, `attribution` when the driver is the address's own tag, or `unknown` when the provider did not report a direction.""" + + exposure_type: Literal["direct", "indirect", "unknown", "attribution"] | str + """`direct` (1 hop) or `indirect` (multi-hop), `attribution` for the address's own tag, or `unknown` when the provider did not state how many hops away the counterparty is. Treat `unknown` as unclassified, not as `direct`.""" + + amount_usd: float | None + """USD value attributed to this driver.""" + + percentage: float + """Share of the address's TOTAL screened volume, both directions combined - unlike `percentage` inside `source_of_funds`/`destination_of_funds`, which is normalised per direction. Comparable and summable across the whole list.""" + + is_high_risk: bool + """Whether the category is one Didit treats as high risk (`sanctioned`, `mixer`, `stolen_funds`, `ransomware`, `scam`, `darknet_market`, `terrorist_financing`, `child_exploitation`, `hacked_exchange`, `high_risk_exchange`, `seized_funds`).""" + + description: str + """One-line rendering of the driver, e.g. `9.01% indirect outgoing exposure to Gambling / Crypto only (Example Gambling)`.""" + + +class WalletScreeningScreenPdfParamsSourceOfFunds(TypedDict, total=False): + category: str + """Normalised risk category, e.g. `exchange_licensed`, `dex`, `defi`, `payment_processor`, `mixer`, `darknet_market`, `sanctioned`, `stolen_funds`, `high_risk_exchange`.""" + + percentage: float + """Share of the direction's total exposure, 0-100.""" + + amount_usd: float | None + """USD value attributed to this entity.""" + + entity_name: str | None + """Name of the attributed entity, for example an exchange, a mixer or a sanctioned group.""" + + entity_type: str | None + """Provider entity type (e.g. `Exchange`, `DeFi`, `Sanctions`, `Theft`).""" + + entity_subtype: str | None + """More granular subtype (e.g. `Mandatory KYC and AML`, `OFAC`, `Yield Aggregator`).""" + + hops: int | None + """Number of hops to the entity (1 for a direct counterparty), when the provider reports one. Always `null` for Merkle Science, which reports only whether exposure is direct or indirect: use `exposure_type`.""" + + description: str | None + """`entity_type / entity_subtype`, as the provider labels the entity.""" + + is_direct: bool + """True when `exposure_type` is `direct`.""" + + exposure_direction: Literal["incoming", "outgoing", "connected"] | str + """Whether the funds came from (`incoming`) or went to (`outgoing`) this entity.""" + + exposure_type: Literal["direct", "indirect"] | str + """`direct` (1 hop) or `indirect` (multi-hop) exposure.""" + + provider_entity_id: str | None + """The provider's own identifier for the entity.""" + + provider_entity_type_id: str | None + """The provider's own identifier for `entity_type`.""" + + provider_entity_subtype_id: str | None + """The provider's own identifier for `entity_subtype`.""" + + country: str | None + """Primary ISO 3166-1 alpha-2 country of the entity. Empty or `null` when the provider does not know it.""" + + raw_amount: float | None + """Amount in the asset's own unit, when the provider reports it.""" + + raw_currency: str | None + """Unit of `raw_amount`, when the provider reports it.""" + + +class WalletScreeningScreenPdfParams(TypedDict, total=False): + """Same shape as the `POST /v3/wallet-screening/` response - pass that response body back verbatim, including `report_signature`.""" + + provider: Required[str] + """Provider that performed the screening (e.g. `merklescience`, `crystal`).""" + + screening_type: Required[Literal["WALLET_SCREENING"] | str] + """Always `WALLET_SCREENING` for this endpoint.""" + + risk_score: Required[int] + """Normalised 0-100 risk score. Higher means greater exposure to risky entities. Merkle Science grades an address 0-5, which maps to 0, 15, 35, 55, 75 or 95.""" + + severity: Required[Literal["UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"] | str] + """Risk bucket derived from `risk_score`: `0-9` UNKNOWN, `10-39` LOW, `40-69` MEDIUM, `70-89` HIGH, `90-100` CRITICAL. `UNKNOWN` is the lowest band, not a separate no-data state: `risk_score` 0 (the common clean-address case) means no adverse assessment, while a non-zero score in the 1-9 range is a real but sub-LOW signal - read the `risk_score`, not just the band. Never treat `UNKNOWN` as an affirmative low-risk or clear rating; do not display it as a pass.""" + + status: Required[Literal["SCREENED", "PENDING", "ERROR"] | str] + """Screening outcome status.""" + + summary: Required[str] + """Human-readable summary of the screening result.""" + + wallet_address: Required[str] + """The screened address, echoed back.""" + + transaction_hash: str | None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + blockchain: Required[str] + """The blockchain that was screened.""" + + block_timestamp: str | None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + total_value_usd: float | None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + fee_usd: float | None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + sender_rows: Sequence[Mapping[str, object]] + """Transaction screening field, shared with the result shape. Always empty on a wallet screening result.""" + + receiver_rows: Sequence[Mapping[str, object]] + """Transaction screening field, shared with the result shape. Always empty on a wallet screening result.""" + + transaction_flows: Sequence[Mapping[str, object]] + """Transaction screening field, shared with the result shape. Always empty on a wallet screening result.""" + + transaction_flows_status: str | None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + transaction_flows_error: str | None + """Transaction screening field, shared with the result shape. Always `null` on a wallet screening result.""" + + source_of_funds: Required[Sequence[WalletScreeningScreenPdfParamsSourceOfFunds]] + """Where the address received funds from, attributed by entity. Each entry is an exposure breakdown.""" + + destination_of_funds: Required[Sequence[WalletScreeningScreenPdfParamsDestinationOfFunds]] + """Where the address sent funds to, attributed by entity. Same item shape as `source_of_funds` with `exposure_direction` = `outgoing`.""" + + counterparty_connections: Required[ + Sequence[WalletScreeningScreenPdfParamsCounterpartyConnections] + ] + """Entities the provider attributes the screened address to. For Merkle Science these come from the address's own owner and user tags, so the same entity can appear twice, and they carry no fund flows: `received_usd`, `sent_usd`, `received_hops` and `sent_hops` are `null` and `percentage` is 0. Do not route on `risk_level` or `is_direct` here: use `sanctions_hit`, `dominant_risk_category` and `risk_factors[].is_high_risk` for the verdict, and `exposure_type` in `source_of_funds`/`destination_of_funds` for directness.""" + + network_nodes: Sequence[Mapping[str, object]] + """Exposure graph nodes for transaction screening. Empty on a wallet screening result.""" + + network_edges: Sequence[Mapping[str, object]] + """Exposure graph edges for transaction screening. Empty on a wallet screening result.""" + + sanctions_hit: Required[bool] + """True if the address has direct or indirect sanctions exposure.""" + + pep_counterparty: bool | None + """Politically exposed person exposure. `null` when no PEP finding is available, which is every result today because no provider reports one; `false` only for an explicit negative screening result. Do not read `null` as a completed PEP check.""" + + dominant_risk_category: Required[str | None] + """Highest-weighted high-risk category, or `null` when none is dominant (e.g. `sanctioned`, `mixer`, `stolen_funds`).""" + + risk_factors: Sequence[WalletScreeningScreenPdfParamsRiskFactors] + """Ranked explanation of `risk_score`: what produced this verdict. Providers grade an address on their own model and return only a verdict (Merkle Science returns an integer 0-5), so the score alone is not actionable. Ordered high-risk first, then by the value that moved through each driver, capped at the five strongest; `source_of_funds` and `destination_of_funds` always carry the complete breakdown. A high `risk_score` with no `is_high_risk` entry means the provider reported no sanctions or attributed-entity match and graded the address purely on its own exposure model - `summary` states this explicitly.""" + + raw_response: Mapping[str, object] + """The provider's own payload. Its shape is provider-specific and can change without notice: read the normalised fields instead of parsing it.""" + + report_signature: Required[str] + """Opaque HMAC-SHA256 signature over this exact result, scoped to your application. Not meaningful on its own - repost the full response body, unmodified and including this field, to `POST /v3/wallet-screening/pdf/` to render it as a PDF. The PDF endpoint rejects the request with `400` if `report_signature` is missing or if any signed field (e.g. `risk_score`, `severity`, `sanctions_hit`) was edited before repost.""" diff --git a/src/didit/types/webhooks/__init__.py b/src/didit/types/webhooks/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/webhooks/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/webhooks/destinations/__init__.py b/src/didit/types/webhooks/destinations/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/webhooks/destinations/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/webhooks/destinations/create_params.py b/src/didit/types/webhooks/destinations/create_params.py new file mode 100644 index 0000000..3e9f197 --- /dev/null +++ b/src/didit/types/webhooks/destinations/create_params.py @@ -0,0 +1,27 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import Required, TypedDict + +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEventsParam + +__all__ = ["WebhooksDestinationsCreateParams"] + + +class WebhooksDestinationsCreateParams(TypedDict, total=False): + label: Required[str] + """Human-readable name displayed in Console → Webhooks. Helps distinguish destinations when you have several (e.g. "Prod", "Staging mirror").""" + + url: Required[str] + """HTTPS URL that will receive `POST` requests. Must be unique per application among non-deleted destinations. Use a stable, hardened endpoint - Didit retries failed deliveries on a backoff schedule.""" + + enabled: bool + """When `false`, the destination is saved but Didit will not push any webhooks to it. Useful for staging configs you want to keep parked.""" + + webhook_version: Literal["v1", "v2", "v3"] | str + """Payload schema version Didit will send. `v3` is the current/recommended format; older versions are kept for backward compatibility with legacy integrations.""" + + subscribed_events: WebhookSubscribedEventsParam + """Events to deliver. Effectively required: an explicit `[]` is rejected with 400, and omitting the field creates a destination with no subscriptions that receives nothing.""" diff --git a/src/didit/types/webhooks/destinations/create_response.py b/src/didit/types/webhooks/destinations/create_response.py new file mode 100644 index 0000000..aaad01b --- /dev/null +++ b/src/didit/types/webhooks/destinations/create_response.py @@ -0,0 +1,54 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEvents + +__all__ = ["WebhooksDestinationsCreateResponse", "WebhooksDestinationsCreateResponseSummary"] + + +class WebhooksDestinationsCreateResponseSummary(BaseModel): + """Delivery health summary (all zero/null on a brand-new destination).""" + + total_deliveries: int | None = None + + failed_deliveries: int | None = None + + error_rate_percentage: int | None = None + + min_response_time_ms: int | None = None + + avg_response_time_ms: int | None = None + + max_response_time_ms: int | None = None + + last_delivery_at: _dt.datetime | None = None + + +class WebhooksDestinationsCreateResponse(BaseModel): + uuid: str | None = None + """Stable destination identifier. Use as `{destination_uuid}` in subsequent calls.""" + + label: str | None = None + + url: str | None = None + + enabled: bool | None = None + + webhook_version: Literal["v1", "v2", "v3"] | str | None = None + + secret_shared_key: str | None = None + """Auto-generated HMAC signing secret bound to this destination (43-character URL-safe token). Use it to verify the signature headers on incoming webhooks. Always returned in full to API-key callers; Console users need the `read:webhooks` permission, otherwise they see an empty string.""" + + subscribed_events: WebhookSubscribedEvents | None = None + """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + summary: WebhooksDestinationsCreateResponseSummary | None = None + """Delivery health summary (all zero/null on a brand-new destination).""" diff --git a/src/didit/types/webhooks/destinations/list_response.py b/src/didit/types/webhooks/destinations/list_response.py new file mode 100644 index 0000000..fdb3a21 --- /dev/null +++ b/src/didit/types/webhooks/destinations/list_response.py @@ -0,0 +1,52 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEvents + +__all__ = ["WebhooksDestinationsListResponseItem"] + + +class WebhooksDestinationsListResponseItem(BaseModel): + uuid: str | None = None + """Stable destination identifier; use as `{destination_uuid}` for update/delete.""" + + label: str | None = None + """Human-readable name shown in the Console.""" + + url: str | None = None + """Destination URL that receives `POST` requests with the webhook payload.""" + + enabled: bool | None = None + """When `false`, Didit silently skips delivery to this destination.""" + + webhook_version: Literal["v1", "v2", "v3"] | str | None = None + """Payload schema version sent to this destination. `v3` is the current/recommended format.""" + + subscribed_events: WebhookSubscribedEvents | None = None + """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + + subscribed_events_count: int | None = None + """Cached length of `subscribed_events`, useful for UI rendering.""" + + deliveries_count: int | None = None + """Total delivery attempts (successful + failed) recorded for this destination.""" + + failed_deliveries_count: int | None = None + """Delivery attempts whose target responded with HTTP ≥ 400 (or did not respond).""" + + average_response_time_ms: int | None = None + """Average target response time in milliseconds, rounded to an integer. `null` if no deliveries yet.""" + + error_rate_percentage: int | None = None + """`round(failed_deliveries_count / deliveries_count * 100)`; `0` when there are no deliveries.""" + + last_delivery_at: _dt.datetime | None = None + """Timestamp of the most recent delivery attempt (success or failure). `null` if never delivered.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None diff --git a/src/didit/types/webhooks/destinations/retrieve_response.py b/src/didit/types/webhooks/destinations/retrieve_response.py new file mode 100644 index 0000000..7e2613b --- /dev/null +++ b/src/didit/types/webhooks/destinations/retrieve_response.py @@ -0,0 +1,53 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEvents + +__all__ = ["WebhooksDestinationsRetrieveResponse", "WebhooksDestinationsRetrieveResponseSummary"] + + +class WebhooksDestinationsRetrieveResponseSummary(BaseModel): + """Delivery health metrics aggregated over the destination's lifetime.""" + + total_deliveries: int | None = None + + failed_deliveries: int | None = None + + error_rate_percentage: int | None = None + + min_response_time_ms: int | None = None + + avg_response_time_ms: int | None = None + + max_response_time_ms: int | None = None + + last_delivery_at: _dt.datetime | None = None + + +class WebhooksDestinationsRetrieveResponse(BaseModel): + uuid: str | None = None + + label: str | None = None + + url: str | None = None + + enabled: bool | None = None + + webhook_version: Literal["v1", "v2", "v3"] | str | None = None + + secret_shared_key: str | None = None + """HMAC signing secret (43-character URL-safe token). Always returned in full to API-key callers; Console users need `read:webhooks`, otherwise an empty string.""" + + subscribed_events: WebhookSubscribedEvents | None = None + """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + summary: WebhooksDestinationsRetrieveResponseSummary | None = None + """Delivery health metrics aggregated over the destination's lifetime.""" diff --git a/src/didit/types/webhooks/destinations/update_params.py b/src/didit/types/webhooks/destinations/update_params.py new file mode 100644 index 0000000..e139a0d --- /dev/null +++ b/src/didit/types/webhooks/destinations/update_params.py @@ -0,0 +1,27 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal + +from typing_extensions import TypedDict + +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEventsParam + +__all__ = ["WebhooksDestinationsUpdateParams"] + + +class WebhooksDestinationsUpdateParams(TypedDict, total=False): + label: str + """New human-readable name.""" + + url: str + """New target URL. Must be unique within the application.""" + + enabled: bool + """Set to `false` to pause deliveries to this destination without deleting it.""" + + webhook_version: Literal["v1", "v2", "v3"] | str + """New payload schema version for future deliveries.""" + + subscribed_events: WebhookSubscribedEventsParam + """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" diff --git a/src/didit/types/webhooks/destinations/update_response.py b/src/didit/types/webhooks/destinations/update_response.py new file mode 100644 index 0000000..7fc13e7 --- /dev/null +++ b/src/didit/types/webhooks/destinations/update_response.py @@ -0,0 +1,49 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +import datetime as _dt +from typing import Literal + +from didit._models import BaseModel +from didit.types.components.webhook_subscribed_events import WebhookSubscribedEvents + +__all__ = ["WebhooksDestinationsUpdateResponse", "WebhooksDestinationsUpdateResponseSummary"] + + +class WebhooksDestinationsUpdateResponseSummary(BaseModel): + total_deliveries: int | None = None + + failed_deliveries: int | None = None + + error_rate_percentage: int | None = None + + min_response_time_ms: int | None = None + + avg_response_time_ms: int | None = None + + max_response_time_ms: int | None = None + + last_delivery_at: _dt.datetime | None = None + + +class WebhooksDestinationsUpdateResponse(BaseModel): + uuid: str | None = None + + label: str | None = None + + url: str | None = None + + enabled: bool | None = None + + webhook_version: Literal["v1", "v2", "v3"] | str | None = None + + secret_shared_key: str | None = None + + subscribed_events: WebhookSubscribedEvents | None = None + """Event filter for this webhook destination. Didit delivers only webhooks whose event type exactly matches one of these values - there is no wildcard subscription. When sent, the list must contain at least one valid event (an explicit `[]` is rejected with 400); a destination whose list is empty (field omitted at create) receives nothing. On update the list is replaced wholesale, never merged.""" + + created_at: _dt.datetime | None = None + + updated_at: _dt.datetime | None = None + + summary: WebhooksDestinationsUpdateResponseSummary | None = None diff --git a/src/didit/types/workflows/__init__.py b/src/didit/types/workflows/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/src/didit/types/workflows/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/src/didit/types/workflows/create_params.py b/src/didit/types/workflows/create_params.py new file mode 100644 index 0000000..2e54b9f --- /dev/null +++ b/src/didit/types/workflows/create_params.py @@ -0,0 +1,47 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import Required, TypedDict + +from didit.types.components.simple_workflow_feature import SimpleWorkflowFeature + +__all__ = ["WorkflowsCreateParams"] + + +class WorkflowsCreateParams(TypedDict, total=False): + """Create a simple linear workflow. The API converts this feature list into a node-based workflow graph internally. STRICT field whitelist: any key not listed here (including `workflow_type`) is rejected with a 400.""" + + workflow_label: str + """Display name for the workflow (max 50 characters). Defaults to `"ID Verification"` when omitted - always set it so workflows stay distinguishable.""" + + is_default: bool + """Set this workflow as the default for new sessions.""" + + status: Literal["draft", "published"] | str + """Omit this field to create a published workflow ready for sessions. Use `draft` only when you want to save without publishing.""" + + features: Required[Sequence[SimpleWorkflowFeature]] + """Verification features in execution order. The API links each item to the next one and adds one final status node automatically.""" + + is_white_label_enabled: bool + """Enable white-label customization for sessions created with this workflow.""" + + is_desktop_allowed: bool + """Allow the verification flow to run on desktop browsers.""" + + max_retry_attempts: int + """Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline.""" + + retry_window_days: int | None + """Rolling window in days used to count retries. `null` enforces an all-time limit.""" + + face_liveness_max_attempts: int + """Maximum liveness submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it.""" + + face_match_max_attempts: int + """Maximum face-match submissions per session. Default 3: one initial attempt plus two retries. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it.""" + + session_expiration_time: int + """Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200).""" diff --git a/src/didit/types/workflows/list_response.py b/src/didit/types/workflows/list_response.py new file mode 100644 index 0000000..7fb2c2e --- /dev/null +++ b/src/didit/types/workflows/list_response.py @@ -0,0 +1,20 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from didit._models import BaseModel +from didit.types.components.workflow_list_item import WorkflowListItem + +__all__ = ["WorkflowsListResponse"] + + +class WorkflowsListResponse(BaseModel): + count: int | None = None + """Total number of workflow versions for the application.""" + + next: str | None = None + """URL of the next page, or `null` on the last page.""" + + previous: str | None = None + """URL of the previous page, or `null` on the first page.""" + + results: list[WorkflowListItem] | None = None diff --git a/src/didit/types/workflows/update_params.py b/src/didit/types/workflows/update_params.py new file mode 100644 index 0000000..2ef8e2c --- /dev/null +++ b/src/didit/types/workflows/update_params.py @@ -0,0 +1,47 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +from __future__ import annotations + +from typing import Literal, Sequence + +from typing_extensions import TypedDict + +from didit.types.components.simple_workflow_feature import SimpleWorkflowFeature + +__all__ = ["WorkflowsUpdateParams"] + + +class WorkflowsUpdateParams(TypedDict, total=False): + """Update workflow metadata or replace the full linear feature list. STRICT field whitelist: any key not listed here is rejected with a 400.""" + + workflow_label: str + """Display name for the workflow (max 50 characters).""" + + is_default: bool + """Set this workflow as the default for new sessions.""" + + status: Literal["draft", "published"] | str + """Controls versioning. Omit to update the current version in place (drafts auto-publish). `published` on an already-published version creates and returns a NEW version. `draft` is only valid while the version is still a draft.""" + + features: Sequence[SimpleWorkflowFeature] + """If provided, this replaces the full feature order and configuration.""" + + is_white_label_enabled: bool + """Enable white-label customization for sessions created with this workflow.""" + + is_desktop_allowed: bool + """Allow the verification flow to run on desktop browsers.""" + + max_retry_attempts: int + """Maximum retry attempts allowed after a declined verification. `0` blocks the user after the first decline.""" + + retry_window_days: int | None + """Rolling window in days used to count retries. `null` enforces an all-time limit.""" + + face_liveness_max_attempts: int + """Maximum liveness submissions per session. This value is also copied into generated liveness and age-estimation workflow nodes unless the feature-level config overrides it.""" + + face_match_max_attempts: int + """Maximum face-match submissions per session. This value is also copied into generated face-match workflow nodes unless the feature-level config overrides it.""" + + session_expiration_time: int + """Seconds before an unfinished session expires. Minimum 1 hour (3600), maximum 4 weeks (2419200).""" diff --git a/src/didit/webhooks/__init__.py b/src/didit/webhooks/__init__.py new file mode 100644 index 0000000..e1b67bc --- /dev/null +++ b/src/didit/webhooks/__init__.py @@ -0,0 +1,59 @@ +"""Verify Didit webhook deliveries and read them as typed events (docs/DESIGN.md section 9). + + import didit + + event = didit.webhooks.verify(request_body_bytes, request.headers, secret=WEBHOOK_SECRET) + if isinstance(event, didit.webhooks.SessionStatusUpdatedEvent): + ... + +The verifier has no framework dependency; see ``examples/`` for FastAPI, Flask +and Django receivers. +""" + +from .._exceptions import WebhookVerificationError, WebhookVerificationReason +from ._canonical import canonical_json +from ._events import ( + ActivityCreatedEvent, + BusinessDataUpdatedEvent, + BusinessStatusUpdatedEvent, + KybRegistrySearchResolvedEvent, + SessionDataUpdatedEvent, + SessionStatusUpdatedEvent, + TransactionCreatedEvent, + TransactionStatusUpdatedEvent, + TravelRuleStatusUpdatedEvent, + UnknownEvent, + UserDataUpdatedEvent, + UserStatusUpdatedEvent, + WebhookEvent, + WebhookEventBase, + WorkflowComplianceChangedEvent, + parse_event, +) +from ._verify import DEFAULT_TOLERANCE, HeadersLike, verify, verify_parsed + +__all__ = [ + "DEFAULT_TOLERANCE", + "ActivityCreatedEvent", + "BusinessDataUpdatedEvent", + "BusinessStatusUpdatedEvent", + "HeadersLike", + "KybRegistrySearchResolvedEvent", + "SessionDataUpdatedEvent", + "SessionStatusUpdatedEvent", + "TransactionCreatedEvent", + "TransactionStatusUpdatedEvent", + "TravelRuleStatusUpdatedEvent", + "UnknownEvent", + "UserDataUpdatedEvent", + "UserStatusUpdatedEvent", + "WebhookEvent", + "WebhookEventBase", + "WebhookVerificationError", + "WebhookVerificationReason", + "WorkflowComplianceChangedEvent", + "canonical_json", + "parse_event", + "verify", + "verify_parsed", +] diff --git a/src/didit/webhooks/_canonical.py b/src/didit/webhooks/_canonical.py new file mode 100644 index 0000000..e8b5a26 --- /dev/null +++ b/src/didit/webhooks/_canonical.py @@ -0,0 +1,81 @@ +"""Canonical JSON, the string ``X-Signature-V2`` signs (docs/DESIGN.md section 9.2). + +It is exactly ``json.dumps(shorten_floats(value), sort_keys=True, +separators=(",", ":"), ensure_ascii=False)``, the backend's own serialisation. +""" + +from __future__ import annotations + +import json +import math +from typing import Any, cast + +from .._exceptions import WebhookVerificationError + +__all__ = ["canonical_json", "parse_body", "shorten_floats"] + + +def shorten_floats(value: object) -> object: + """Write floats with an integral value as integers (``87.0`` -> ``87``), recursively.""" + if isinstance(value, dict): + return {k: shorten_floats(v) for k, v in cast("dict[str, object]", value).items()} + if isinstance(value, list): + return [shorten_floats(v) for v in cast("list[object]", value)] + if isinstance(value, float) and value.is_integer(): + return int(value) + return value + + +def _check_finite(value: object) -> None: + stack = [value] + while stack: + cur = stack.pop() + if isinstance(cur, float) and not math.isfinite(cur): + raise WebhookVerificationError( + "invalid_payload", "The webhook body holds a non-finite number." + ) + if isinstance(cur, dict): + stack.extend(cast("dict[str, object]", cur).values()) + elif isinstance(cur, list): + stack.extend(cast("list[object]", cur)) + + +def canonical_json(value: object) -> str: + """The canonical JSON text of a parsed webhook body.""" + _check_finite(value) + try: + return json.dumps( + shorten_floats(value), + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + allow_nan=False, + ) + except (TypeError, ValueError, RecursionError) as exc: + raise WebhookVerificationError( + "invalid_payload", "The webhook payload is not JSON data Didit sends." + ) from exc + + +def _reject_constant(name: str) -> Any: + raise ValueError(f"{name} is not valid JSON") + + +def parse_body(raw: bytes) -> dict[str, Any]: + """Parse a webhook body strictly: UTF-8, standard JSON (no ``NaN``), an object.""" + try: + text = raw.decode("utf-8") + value = json.loads(text, parse_constant=_reject_constant) + except ( + UnicodeDecodeError, + ValueError, + RecursionError, + ) as exc: # RecursionError: absurd nesting + raise WebhookVerificationError( + "invalid_payload", "The webhook body is not valid JSON." + ) from exc + if not isinstance(value, dict): + raise WebhookVerificationError("invalid_payload", "The webhook body is not a JSON object.") + body = cast("dict[str, Any]", value) + _check_finite(body) + return body diff --git a/src/didit/webhooks/_events.py b/src/didit/webhooks/_events.py new file mode 100644 index 0000000..9990ce3 --- /dev/null +++ b/src/didit/webhooks/_events.py @@ -0,0 +1,229 @@ +"""Typed webhook events (docs/DESIGN.md section 9.4).""" + +from __future__ import annotations + +from typing import Any, Literal, Union + +from pydantic import Field, PrivateAttr + +from .._models import BaseModel, validate_response +from ..types.sessions.retrieve_decision_response import SessionsRetrieveDecisionResponse + +__all__ = [ + "ActivityCreatedEvent", + "BusinessDataUpdatedEvent", + "BusinessStatusUpdatedEvent", + "KybRegistrySearchResolvedEvent", + "SessionDataUpdatedEvent", + "SessionStatusUpdatedEvent", + "TransactionCreatedEvent", + "TransactionStatusUpdatedEvent", + "TravelRuleStatusUpdatedEvent", + "UnknownEvent", + "UserDataUpdatedEvent", + "UserStatusUpdatedEvent", + "WebhookEvent", + "WebhookEventBase", + "WorkflowComplianceChangedEvent", + "parse_event", +] + +VerifiedBy = Literal["signature", "signature_v2", "signature_simple", "none"] + + +class WebhookEventBase(BaseModel): + """Fields on every event. Unknown fields are kept (``model_extra``).""" + + event_id: str | None = None + webhook_type: str | None = None + timestamp: int | None = None + created_at: int | None = None + application_id: str | None = None + environment: Literal["live", "sandbox"] | str | None = None + + _verified_by: VerifiedBy = PrivateAttr(default="none") + + @property + def verified_by(self) -> VerifiedBy: + """Which header authenticated the delivery: ``signature`` (``X-Signature``), + ``signature_v2`` (``X-Signature-V2``) or ``signature_simple``.""" + return self._verified_by + + @property + def body_authenticated(self) -> bool: + """``False`` when only the deprecated ``X-Signature-Simple`` matched: it + signs the envelope, not the body, so re-fetch the record from the API + before acting on anything else in this event.""" + return self._verified_by in ("signature", "signature_v2") + + +class _SessionEvent(WebhookEventBase): + session_id: str | None = None + business_session_id: str | None = None + session_kind: Literal["user", "business"] | str | None = None + status: str | None = None + workflow_id: str | None = None + workflow_version: int | None = None + vendor_data: str | None = None + metadata: object = None + decision: SessionsRetrieveDecisionResponse | None = None + resubmit_info: object = None + + +class SessionStatusUpdatedEvent(_SessionEvent): + """``status.updated``: a verification session changed status.""" + + webhook_type: Literal["status.updated"] = "status.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class SessionDataUpdatedEvent(_SessionEvent): + """``data.updated``: the data of a verification session changed.""" + + webhook_type: Literal["data.updated"] = "data.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class _UserEvent(WebhookEventBase): + vendor_user_id: str | None = None + vendor_data: str | None = None + status: str | None = None + + +class UserStatusUpdatedEvent(_UserEvent): + """``user.status.updated``.""" + + webhook_type: Literal["user.status.updated"] = "user.status.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class UserDataUpdatedEvent(_UserEvent): + """``user.data.updated``.""" + + webhook_type: Literal["user.data.updated"] = "user.data.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class _BusinessEvent(WebhookEventBase): + vendor_business_id: str | None = None + vendor_data: str | None = None + status: str | None = None + + +class BusinessStatusUpdatedEvent(_BusinessEvent): + """``business.status.updated``.""" + + webhook_type: Literal["business.status.updated"] = "business.status.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class BusinessDataUpdatedEvent(_BusinessEvent): + """``business.data.updated``.""" + + webhook_type: Literal["business.data.updated"] = "business.data.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class ActivityCreatedEvent(WebhookEventBase): + """``activity.created``.""" + + webhook_type: Literal["activity.created"] = "activity.created" # pyright: ignore[reportIncompatibleVariableOverride] + + +class _TransactionEvent(WebhookEventBase): + transaction_id: str | None = None + txn_id: str | None = None + status: str | None = None + score: float | None = None + severity: str | None = None + amount: str | None = None + """A decimal string, as the API sends it.""" + currency: str | None = None + direction: str | None = None + action_required: object = None + + +class TransactionCreatedEvent(_TransactionEvent): + """``transaction.created``.""" + + webhook_type: Literal["transaction.created"] = "transaction.created" # pyright: ignore[reportIncompatibleVariableOverride] + + +class TransactionStatusUpdatedEvent(_TransactionEvent): + """``transaction.status.updated``.""" + + webhook_type: Literal["transaction.status.updated"] = "transaction.status.updated" # pyright: ignore[reportIncompatibleVariableOverride] + + +class TravelRuleStatusUpdatedEvent(WebhookEventBase): + """``travel_rule.status.updated``.""" + + webhook_type: Literal["travel_rule.status.updated"] = "travel_rule.status.updated" # pyright: ignore[reportIncompatibleVariableOverride] + transaction_id: str | None = None + txn_id: str | None = None + travel_rule_status: str | None = None + rail: str | None = None + direction: str | None = None + + +class WorkflowComplianceChangedEvent(WebhookEventBase): + """``workflow.compliance_changed``.""" + + webhook_type: Literal["workflow.compliance_changed"] = "workflow.compliance_changed" # pyright: ignore[reportIncompatibleVariableOverride] + workflow_id: str | None = None + + +class KybRegistrySearchResolvedEvent(WebhookEventBase): + """``kyb.registry_search.resolved``.""" + + webhook_type: Literal["kyb.registry_search.resolved"] = "kyb.registry_search.resolved" # pyright: ignore[reportIncompatibleVariableOverride] + + +class UnknownEvent(WebhookEventBase): + """An event type this version of the SDK does not know. ``payload`` holds + the whole body.""" + + payload: dict[str, Any] = Field(default_factory=dict) + + +WebhookEvent = Union[ + SessionStatusUpdatedEvent, + SessionDataUpdatedEvent, + UserStatusUpdatedEvent, + UserDataUpdatedEvent, + BusinessStatusUpdatedEvent, + BusinessDataUpdatedEvent, + ActivityCreatedEvent, + TransactionCreatedEvent, + TransactionStatusUpdatedEvent, + TravelRuleStatusUpdatedEvent, + WorkflowComplianceChangedEvent, + KybRegistrySearchResolvedEvent, + UnknownEvent, +] +"""Every webhook event; narrow it with ``isinstance`` or ``match``.""" + +_BY_TYPE: dict[str, type[WebhookEventBase]] = { + "status.updated": SessionStatusUpdatedEvent, + "data.updated": SessionDataUpdatedEvent, + "user.status.updated": UserStatusUpdatedEvent, + "user.data.updated": UserDataUpdatedEvent, + "business.status.updated": BusinessStatusUpdatedEvent, + "business.data.updated": BusinessDataUpdatedEvent, + "activity.created": ActivityCreatedEvent, + "transaction.created": TransactionCreatedEvent, + "transaction.status.updated": TransactionStatusUpdatedEvent, + "travel_rule.status.updated": TravelRuleStatusUpdatedEvent, + "workflow.compliance_changed": WorkflowComplianceChangedEvent, + "kyb.registry_search.resolved": KybRegistrySearchResolvedEvent, +} + + +def parse_event(payload: dict[str, Any], verified_by: VerifiedBy = "none") -> WebhookEvent: + """Turn a webhook body into its typed event, never raising on unknown data.""" + kind = payload.get("webhook_type") + if not isinstance(kind, str): + kind = payload.get("event_type") if isinstance(payload.get("event_type"), str) else None + cls = _BY_TYPE.get(kind) if isinstance(kind, str) else None + event: WebhookEventBase + if cls is None: + envelope = {k: payload[k] for k in WebhookEventBase.model_fields if k in payload} + event = validate_response(UnknownEvent, {**envelope, "payload": payload}) + else: + event = validate_response(cls, payload) + event._verified_by = verified_by # pyright: ignore[reportPrivateUsage] + return event # type: ignore[return-value] diff --git a/src/didit/webhooks/_mixin.py b/src/didit/webhooks/_mixin.py new file mode 100644 index 0000000..29dd8f3 --- /dev/null +++ b/src/didit/webhooks/_mixin.py @@ -0,0 +1,39 @@ +"""``client.webhooks.verify(...)``: the verifier, reachable from the client too.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any + +from ._events import WebhookEvent +from ._verify import DEFAULT_TOLERANCE, HeadersLike, verify, verify_parsed + +__all__ = ["WebhooksMixin"] + + +class WebhooksMixin: + """Adds :func:`didit.webhooks.verify` and :func:`didit.webhooks.verify_parsed` + to ``client.webhooks``. They need no API key and send no request.""" + + def verify( + self, + raw_body: bytes | bytearray | memoryview | str, + headers: HeadersLike, + secret: str | Sequence[str] | None, + *, + tolerance: int = DEFAULT_TOLERANCE, + allow_simple: bool = False, + ) -> WebhookEvent: + """See :func:`didit.webhooks.verify`.""" + return verify(raw_body, headers, secret, tolerance=tolerance, allow_simple=allow_simple) + + def verify_parsed( + self, + payload: Mapping[str, Any], + headers: HeadersLike, + secret: str | Sequence[str] | None, + *, + tolerance: int = DEFAULT_TOLERANCE, + ) -> WebhookEvent: + """See :func:`didit.webhooks.verify_parsed`.""" + return verify_parsed(payload, headers, secret, tolerance=tolerance) diff --git a/src/didit/webhooks/_verify.py b/src/didit/webhooks/_verify.py new file mode 100644 index 0000000..5f9c3c9 --- /dev/null +++ b/src/didit/webhooks/_verify.py @@ -0,0 +1,225 @@ +"""Webhook signature verification (docs/DESIGN.md section 9.3).""" + +from __future__ import annotations + +import hashlib +import hmac +import math +import re +import time +from collections.abc import Iterable, Mapping, Sequence +from typing import Any, Union, cast + +from .._exceptions import ConfigurationError, WebhookVerificationError +from ._canonical import canonical_json, parse_body +from ._events import VerifiedBy, WebhookEvent, parse_event + +__all__ = ["DEFAULT_TOLERANCE", "HeadersLike", "verify", "verify_parsed"] + +DEFAULT_TOLERANCE = 300 +_HEX = re.compile(r"^[0-9a-f]{64}\Z") +_DIGITS = re.compile(r"^[0-9]{1,20}\Z") + +HeadersLike = Union[Mapping[str, Any], Iterable[tuple[str, str]]] +"""The request headers: any mapping (Django ``request.headers``, Starlette and +Flask headers, ``dict``) or a list of ``(name, value)`` pairs. Names are +matched case-insensitively.""" + +_RAW = ("x-signature", "x-signature-previous") +_V2 = ("x-signature-v2", "x-signature-v2-previous") +_SIMPLE = ("x-signature-simple", "x-signature-simple-previous") + + +def _normalise_headers(headers: HeadersLike) -> dict[str, str]: + pairs: Iterable[tuple[Any, Any]] + pairs = cast("Mapping[Any, Any]", headers).items() if isinstance(headers, Mapping) else headers + out: dict[str, str] = {} + for raw_name, raw_value in pairs: + if isinstance(raw_name, bytes): # raw ASGI scope["headers"] + raw_name = raw_name.decode("latin-1") + name = str(raw_name).lower() + if name.startswith("http_"): # WSGI environ / Django request.META + name = name[5:].replace("_", "-") + value = raw_value + if isinstance(value, (list, tuple)): + items = cast("Sequence[Any]", value) + value = items[0] if items else "" + if isinstance(value, bytes): + value = value.decode("latin-1") + out.setdefault(name, str(value).strip()) + return out + + +def _secrets(secret: str | Sequence[str] | None) -> list[bytes]: + if secret is None: + raise ConfigurationError( + "No webhook secret (None); copy it from the webhook destination in the console, " + "for example into DIDIT_WEBHOOK_SECRET." + ) + if isinstance(secret, (bytes, bytearray, memoryview)): + raise ConfigurationError("The webhook secret must be a str, not bytes.") + if isinstance(secret, str): + items: list[object] = [secret] + elif isinstance(secret, Iterable): # pyright: ignore[reportUnnecessaryIsInstance] + items = list(secret) + else: + raise ConfigurationError("The webhook secret must be a str or a list of them.") + if any(not isinstance(s, str) for s in items): + raise ConfigurationError("Every webhook secret must be a str.") + if not items or any(not s for s in items): + raise ConfigurationError( + "The webhook secret is empty; copy it from the webhook destination in the console." + ) + return [cast("str", s).encode("utf-8") for s in items] + + +def _check_tolerance(tolerance: int) -> None: + # ``not >= 0`` also refuses NaN, which would otherwise switch the replay check off. + if isinstance(tolerance, bool) or not isinstance(tolerance, (int, float)) or not tolerance >= 0: # pyright: ignore[reportUnnecessaryIsInstance] + raise ConfigurationError("tolerance must be 0 or more seconds.") + + +def _timestamp( + payload: Mapping[str, Any], headers: Mapping[str, str], tolerance: int, now: float +) -> int: + # Didit writes the timestamp as a JSON integer (floats with an integral value + # are shortened before signing), so anything else is not a Didit delivery. + raw = payload.get("timestamp") + if not isinstance(raw, int) or isinstance(raw, bool): + raise WebhookVerificationError( + "missing_timestamp", "The webhook body has no integer timestamp." + ) + header = headers.get("x-timestamp") + if header is not None: + header_value = int(header) if _DIGITS.match(header) else None + if header_value != raw: + raise WebhookVerificationError( + "timestamp_mismatch", + "X-Timestamp does not match the timestamp in the webhook body.", + ) + try: + skew = abs(now - raw) + except OverflowError: # an integer too large for a float + skew = math.inf + if skew > tolerance: + raise WebhookVerificationError( + "timestamp_out_of_tolerance", + f"The webhook timestamp is more than {tolerance} seconds away from now; it may be a replay.", + ) + return raw + + +def _matches(message: bytes, secrets: list[bytes], candidates: list[str]) -> bool: + good = [c for c in candidates if _HEX.match(c)] + if not good: + return False + for key in secrets: + expected = hmac.new(key, message, hashlib.sha256).hexdigest() + for candidate in good: + if hmac.compare_digest(expected, candidate): + return True + return False + + +def _simple_message(payload: Mapping[str, Any]) -> bytes: + # The backend formats each field with str(), so a JSON null reads "None". + parts = [str(payload.get(k, "")) for k in ("timestamp", "session_id", "status", "webhook_type")] + return ":".join(parts).encode("utf-8") + + +def _canonical_bytes(payload: Mapping[str, Any]) -> bytes: + return canonical_json(payload).encode("utf-8", "surrogatepass") + + +def verify( + raw_body: bytes | bytearray | memoryview | str, + headers: HeadersLike, + secret: str | Sequence[str] | None, + *, + tolerance: int = DEFAULT_TOLERANCE, + allow_simple: bool = False, +) -> WebhookEvent: + """Verify a webhook delivery and return its typed event. + + Pass the exact bytes of the request body. ``secret`` is the destination's + secret, or a list of secrets during a rotation. The delivery is accepted when + ``X-Signature`` matches the body, or ``X-Signature-V2`` matches its canonical + JSON, or one of their ``-Previous`` variants matches. ``X-Signature-Simple`` + (deprecated) counts only with ``allow_simple=True``, and then + ``event.body_authenticated`` is ``False``: re-fetch the record from the API. + + Raises :class:`didit.WebhookVerificationError` (see ``reason``) for anything + the request carries, or :class:`didit.ConfigurationError` for a missing or + empty secret or a negative tolerance. + """ + keys = _secrets(secret) + _check_tolerance(tolerance) + try: + raw = raw_body.encode("utf-8") if isinstance(raw_body, str) else bytes(raw_body) + except UnicodeEncodeError as exc: # a str with a lone surrogate + raise WebhookVerificationError( + "invalid_payload", "The webhook body is not valid UTF-8 text." + ) from exc + payload = parse_body(raw) + normalised = _normalise_headers(headers) + _timestamp(payload, normalised, tolerance, time.time()) + + present = [h for h in (*_RAW, *_V2, *_SIMPLE) if normalised.get(h)] + if not present: + raise WebhookVerificationError( + "missing_signature", "The delivery has no X-Signature header." + ) + verified_by: VerifiedBy | None = None + raw_candidates = [normalised[h] for h in _RAW if normalised.get(h)] + v2_candidates = [normalised[h] for h in _V2 if normalised.get(h)] + simple_candidates = [normalised[h] for h in _SIMPLE if normalised.get(h)] + if raw_candidates and _matches(raw, keys, raw_candidates): + verified_by = "signature" + elif v2_candidates and _matches(_canonical_bytes(payload), keys, v2_candidates): + verified_by = "signature_v2" + elif ( + allow_simple + and simple_candidates + and _matches(_simple_message(payload), keys, simple_candidates) + ): + verified_by = "signature_simple" + if verified_by is None: + raise WebhookVerificationError( + "invalid_signature", "No webhook signature matches the secret." + ) + return parse_event(payload, verified_by) + + +def verify_parsed( + payload: Mapping[str, Any], + headers: HeadersLike, + secret: str | Sequence[str] | None, + *, + tolerance: int = DEFAULT_TOLERANCE, +) -> WebhookEvent: + """Verify a delivery when only the parsed JSON body is available. + + Only ``X-Signature-V2`` (and ``X-Signature-V2-Previous``) can be checked, + because the other signatures cover the exact body bytes. Prefer + :func:`verify` with the raw body whenever the framework gives access to it. + """ + keys = _secrets(secret) + _check_tolerance(tolerance) + if not isinstance(payload, Mapping): # pyright: ignore[reportUnnecessaryIsInstance] + raise WebhookVerificationError( + "invalid_payload", "The webhook payload is not a JSON object." + ) + body = dict(payload) + normalised = _normalise_headers(headers) + _timestamp(body, normalised, tolerance, time.time()) + v2_candidates = [normalised[h] for h in _V2 if normalised.get(h)] + if not v2_candidates: + raise WebhookVerificationError( + "missing_signature", + "verify_parsed needs X-Signature-V2; pass the raw body to verify() to check X-Signature.", + ) + if not _matches(_canonical_bytes(body), keys, v2_candidates): + raise WebhookVerificationError( + "invalid_signature", "No webhook signature matches the secret." + ) + return parse_event(body, "signature_v2") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/authentication_helpers.py b/tests/authentication_helpers.py new file mode 100644 index 0000000..4e1fe26 --- /dev/null +++ b/tests/authentication_helpers.py @@ -0,0 +1,236 @@ +"""A stub Didit Authentication issuer for tests/test_authentication.py. + +Signing keys are generated locally with ``cryptography`` and tokens are signed +here; the issuer is an ``httpx.MockTransport``, so nothing touches the network. +Every id, token and secret below is synthetic. +""" + +from __future__ import annotations + +import asyncio +import base64 +import json +import time +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any + +import httpx +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature + +ISSUER = "https://acme-0a1b2c3d4e5f60718293a4b5c6d7e8f9.auth.didit.me" +APPLICATION_ID = "6f1c2d3e-0000-4000-8000-00000000a001" +IDENTITY_ID = "6f1c2d3e-0000-4000-8000-00000000b001" +SESSION_ID = "6f1c2d3e-0000-4000-8000-00000000c001" +STEP_UP_ID = "6f1c2d3e-0000-4000-8000-00000000d001" +ACCESS_TOKEN = "synthetic-access-token-0001" +REFRESH_TOKEN = "synthetic-refresh-token-0001" +CODE = "synthetic-authorization-code-0001" +CODE_VERIFIER = "synthetic-code-verifier-000000000000000000000001" +REDIRECT_URI = "https://app.example.com/callback" +NONCE = "synthetic-nonce-0001" +NOW = 1_800_000_000 + +REMOVE: Any = object() +"""Pass as a claim override to leave the claim out.""" + +Handler = Callable[[httpx.Request], httpx.Response] + + +def b64url(data: bytes) -> str: + return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def segment(value: object) -> str: + return b64url(json.dumps(value).encode("utf-8")) + + +def unsigned_token( + header: Mapping[str, Any], claims: Mapping[str, Any], signature: bytes = b"" +) -> str: + """A token with any header and signature bytes, for the rejection tests.""" + return f"{segment(dict(header))}.{segment(dict(claims))}.{b64url(signature)}" + + +@dataclass +class SigningKey: + kid: str + private_key: ec.EllipticCurvePrivateKey + + @classmethod + def generate(cls, kid: str) -> SigningKey: + return cls(kid, ec.generate_private_key(ec.SECP256R1())) + + def jwk(self) -> dict[str, str]: + numbers = self.private_key.public_key().public_numbers() + return { + "kty": "EC", + "crv": "P-256", + "x": b64url(numbers.x.to_bytes(32, "big")), + "y": b64url(numbers.y.to_bytes(32, "big")), + "kid": self.kid, + "use": "sig", + "alg": "ES256", + } + + def sign( + self, + claims: Mapping[str, Any], + *, + typ: str | None = "at+jwt", + kid: str | None = None, + header: Mapping[str, Any] | None = None, + ) -> str: + """An ES256 JWS as the Didit issuer signs it (raw r||s signature).""" + head: dict[str, Any] = {"alg": "ES256", "kid": self.kid if kid is None else kid} + if typ is not None: + head["typ"] = typ + head.update(header or {}) + signing_input = f"{segment(head)}.{segment(dict(claims))}" + der = self.private_key.sign(signing_input.encode("ascii"), ec.ECDSA(hashes.SHA256())) + r, s = decode_dss_signature(der) + return f"{signing_input}.{b64url(r.to_bytes(32, 'big') + s.to_bytes(32, 'big'))}" + + +def _claims(base: dict[str, Any], overrides: Mapping[str, Any]) -> dict[str, Any]: + base.update(overrides) + return {name: value for name, value in base.items() if value is not REMOVE} + + +def access_claims(now: int = NOW, **overrides: Any) -> dict[str, Any]: + """The claims of a person's access token (issuer.session_tokens).""" + return _claims( + { + "iss": ISSUER, + "aud": APPLICATION_ID, + "sub": IDENTITY_ID, + "sid": SESSION_ID, + "jti": "synthetic-jti-0001", + "iat": now, + "exp": now + 300, + "vendor_data": "user-42", + "org": {"id": "6f1c2d3e-0000-4000-8000-00000000e001", "role": "admin"}, + "verified": {"level": "full_id", "over_18": True, "country": "ES"}, + "amr": ["passkey"], + "act": None, + "client_id": APPLICATION_ID, + }, + overrides, + ) + + +def id_claims(now: int = NOW, **overrides: Any) -> dict[str, Any]: + """The claims of an ID token (issuer.session_tokens): no client_id, plus profile claims.""" + return _claims( + { + "iss": ISSUER, + "aud": APPLICATION_ID, + "sub": IDENTITY_ID, + "sid": SESSION_ID, + "jti": "synthetic-jti-0002", + "iat": now, + "exp": now + 300, + "vendor_data": "user-42", + "org": None, + "verified": {}, + "amr": ["password"], + "act": None, + "auth_time": now - 10, + "email_verified": True, + "email": "person@example.com", + "name": "Synthetic Person", + "nonce": NONCE, + }, + overrides, + ) + + +class FakeClock: + def __init__(self, start: float = 1000.0) -> None: + self.now = start + + def __call__(self) -> float: + return self.now + + def advance(self, seconds: float) -> None: + self.now += seconds + + +class FakeIssuer: + """The issuer's HTTP side: discovery and JWKS built in, other routes per test.""" + + def __init__(self, keys: list[SigningKey], *, issuer: str = ISSUER) -> None: + self.issuer = issuer + self.keys = keys + self.metadata_cache_control: str | None = "public, max-age=300" + self.discovery: dict[str, Any] = { + "issuer": issuer, + "authorization_endpoint": f"{issuer}/sign-in/", + "token_endpoint": f"{issuer}/token/", + "jwks_uri": f"{issuer}/.well-known/jwks.json", + "response_types_supported": ["code"], + "grant_types_supported": ["authorization_code", "refresh_token"], + "id_token_signing_alg_values_supported": ["ES256"], + "token_endpoint_auth_methods_supported": ["none"], + "code_challenge_methods_supported": ["S256"], + } + self.routes: dict[tuple[str, str], Handler] = {} + self.requests: list[httpx.Request] = [] + self.jwks_delay = 0.0 + + def route(self, method: str, path: str, handler: Handler) -> None: + self.routes[(method, path)] = handler + + def reply(self, method: str, path: str, status: int, body: object = None) -> None: + """Answer ``method path`` with ``status`` and a JSON ``body`` (no body when ``None``).""" + + def handler(_: httpx.Request) -> httpx.Response: + if body is None: + return httpx.Response(status) + return httpx.Response(status, json=body, headers={"x-request-id": "req_synthetic"}) + + self.route(method, path, handler) + + def _metadata(self, body: object) -> httpx.Response: + headers = {"content-type": "application/json"} + if self.metadata_cache_control is not None: + headers["cache-control"] = self.metadata_cache_control + return httpx.Response(200, content=json.dumps(body).encode(), headers=headers) + + def handle(self, request: httpx.Request) -> httpx.Response: + self.requests.append(request) + path = request.url.path + if request.method == "GET" and path == "/.well-known/openid-configuration": + return self._metadata(self.discovery) + if request.method == "GET" and path == "/.well-known/jwks.json": + if self.jwks_delay: + time.sleep(self.jwks_delay) + return self._metadata({"keys": [key.jwk() for key in self.keys]}) + handler = self.routes.get((request.method, path)) + if handler is None: + return httpx.Response(404, json={"detail": "Not found."}) + return handler(request) + + async def handle_async(self, request: httpx.Request) -> httpx.Response: + if self.jwks_delay and request.url.path == "/.well-known/jwks.json": + await asyncio.sleep(self.jwks_delay) + delay, self.jwks_delay = self.jwks_delay, 0.0 + try: + return self.handle(request) + finally: + self.jwks_delay = delay + return self.handle(request) + + def count(self, path: str) -> int: + return sum(1 for request in self.requests if request.url.path == path) + + def last(self, path: str) -> httpx.Request: + return [request for request in self.requests if request.url.path == path][-1] + + def client(self) -> httpx.Client: + return httpx.Client(transport=httpx.MockTransport(self.handle)) + + def async_client(self) -> httpx.AsyncClient: + return httpx.AsyncClient(transport=httpx.MockTransport(self.handle_async)) diff --git a/tests/generated/__init__.py b/tests/generated/__init__.py new file mode 100644 index 0000000..73f4330 --- /dev/null +++ b/tests/generated/__init__.py @@ -0,0 +1 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. diff --git a/tests/generated/test_account.py b/tests/generated/test_account.py new file mode 100644 index 0000000..8d3eade --- /dev/null +++ b/tests/generated/test_account.py @@ -0,0 +1,276 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``account`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.application import Application +from didit.types.components.login_response import LoginResponse +from didit.types.components.organization import Organization +from didit.types.components.register_response import RegisterResponse +from didit.types.components.verify_email_response import VerifyEmailResponse +from tests.harness import Expect, check_operation + + +def test_account_login() -> None: + """``POST /programmatic/login/``""" + check_operation( + Expect( + method="POST", + path="/programmatic/login/", + api="account", + auth="none", + json_body={"email": "you@yourdomain.com", "password": "sample-secret"}, + status=200, + response_json={ + "access_token": "sample-token", + "refresh_token": "sample-token", + "expires_in": 86400, + "message": "Login successful", + }, + ), + lambda c: c.account.login(email="you@yourdomain.com", password="sample-secret"), + lambda c: c.account.login(email="you@yourdomain.com", password="sample-secret"), + lambda r: isinstance(r, LoginResponse), + lambda r: isinstance(r, LoginResponse), + ) + + +def test_account_register() -> None: + """``POST /programmatic/register/``""" + check_operation( + Expect( + method="POST", + path="/programmatic/register/", + api="account", + auth="none", + json_body={"email": "you@yourdomain.com", "password": "sample-secret"}, + status=201, + response_json={ + "message": "Registration successful. Check your email for the verification code.", + "email": "you@yourdomain.com", + }, + ), + lambda c: c.account.register(email="you@yourdomain.com", password="sample-secret"), + lambda c: c.account.register(email="you@yourdomain.com", password="sample-secret"), + lambda r: isinstance(r, RegisterResponse), + lambda r: isinstance(r, RegisterResponse), + ) + + +def test_account_resend_otp() -> None: + """``POST /programmatic/resend-otp/``""" + check_operation( + Expect( + method="POST", + path="/programmatic/resend-otp/", + api="account", + auth="none", + json_body={"email": "dev@example.com", "password": "sample-secret"}, + status=200, + response_json={"detail": "A new verification code has been sent to your email."}, + ), + lambda c: c.account.resend_otp(email="dev@example.com", password="sample-secret"), + lambda c: c.account.resend_otp(email="dev@example.com", password="sample-secret"), + lambda r: True, + lambda r: True, + ) + + +def test_account_verify_email() -> None: + """``POST /programmatic/verify-email/``""" + check_operation( + Expect( + method="POST", + path="/programmatic/verify-email/", + api="account", + auth="none", + json_body={"email": "you@yourdomain.com", "code": "A3K9F2"}, + status=200, + response_json={ + "access_token": "sample-token", + "refresh_token": "sample-token", + "expires_in": 86400, + "organization": { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "name": "developer", + }, + "application": { + "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", + "name": "developer", + "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", + "api_key": "sample-secret", + }, + }, + ), + lambda c: c.account.verify_email(email="you@yourdomain.com", code="A3K9F2"), + lambda c: c.account.verify_email(email="you@yourdomain.com", code="A3K9F2"), + lambda r: isinstance(r, VerifyEmailResponse), + lambda r: isinstance(r, VerifyEmailResponse), + ) + + +def test_account_applications_create() -> None: + """``POST /organizations/me/{org_id}/applications/``""" + check_operation( + Expect( + method="POST", + path="/organizations/me/a1b2c3d4-5678-90ab-cdef-111111111111/applications/", + api="account", + auth="account_token", + json_body={ + "name": "Acme Customer App", + "website_url": "https://acme.example", + "redirect_uris": ["https://acme.example/callback"], + "terms_url": "https://acme.example/terms", + "privacy_url": "https://acme.example/privacy", + }, + status=201, + response_json={ + "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", + "name": "Acme Customer App", + "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", + "api_key": "sample-secret", + "website_url": "https://acme.example", + "redirect_uris": ["https://acme.example/callback"], + "terms_url": "https://acme.example/terms", + "privacy_url": "https://acme.example/privacy", + "description": None, + "created_at": "2025-06-01T10:00:00Z", + }, + ), + lambda c: c.account.applications.create( + "a1b2c3d4-5678-90ab-cdef-111111111111", + name="Acme Customer App", + website_url="https://acme.example", + redirect_uris=["https://acme.example/callback"], + terms_url="https://acme.example/terms", + privacy_url="https://acme.example/privacy", + ), + lambda c: c.account.applications.create( + "a1b2c3d4-5678-90ab-cdef-111111111111", + name="Acme Customer App", + website_url="https://acme.example", + redirect_uris=["https://acme.example/callback"], + terms_url="https://acme.example/terms", + privacy_url="https://acme.example/privacy", + ), + lambda r: isinstance(r, Application), + lambda r: isinstance(r, Application), + ) + + +def test_account_applications_list() -> None: + """``GET /organizations/me/{org_id}/applications/``""" + check_operation( + Expect( + method="GET", + path="/organizations/me/a1b2c3d4-5678-90ab-cdef-111111111111/applications/", + api="account", + auth="account_token", + status=200, + response_json="value", + strict_type=object, + ), + lambda c: c.account.applications.list("a1b2c3d4-5678-90ab-cdef-111111111111"), + lambda c: c.account.applications.list("a1b2c3d4-5678-90ab-cdef-111111111111"), + lambda r: True, + lambda r: True, + ) + + +def test_account_applications_retrieve() -> None: + """``GET /organizations/me/{org_id}/applications/{app_id}/``""" + check_operation( + Expect( + method="GET", + path="/organizations/me/a1b2c3d4-5678-90ab-cdef-111111111111/applications/b2c3d4e5-6789-01bc-defg-222222222222/", + api="account", + auth="account_token", + status=200, + response_json={ + "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", + "name": "My App", + "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", + "api_key": "sample-secret", + "website_url": "https://example.com", + "redirect_uris": ["https://example.com/callback"], + "terms_url": "https://example.com/terms", + "privacy_url": "https://example.com/privacy", + "description": "Production application", + "created_at": "2025-06-01T10:00:00Z", + }, + ), + lambda c: c.account.applications.retrieve( + "a1b2c3d4-5678-90ab-cdef-111111111111", "b2c3d4e5-6789-01bc-defg-222222222222" + ), + lambda c: c.account.applications.retrieve( + "a1b2c3d4-5678-90ab-cdef-111111111111", "b2c3d4e5-6789-01bc-defg-222222222222" + ), + lambda r: isinstance(r, Application), + lambda r: isinstance(r, Application), + ) + + +def test_account_applications_update() -> None: + """``PATCH /organizations/me/{org_id}/applications/{app_id}/``""" + check_operation( + Expect( + method="PATCH", + path="/organizations/me/a1b2c3d4-5678-90ab-cdef-111111111111/applications/b2c3d4e5-6789-01bc-defg-222222222222/", + api="account", + auth="account_token", + json_body={"name": "Acme Production App", "website_url": "https://app.acme.example"}, + status=200, + response_json={ + "uuid": "b2c3d4e5-6789-01bc-defg-222222222222", + "name": "Acme Production App", + "client_id": "S9LIYGSoWNuGMLHsvEt9dQ", + "api_key": "sample-secret", + "website_url": "https://app.acme.example", + "redirect_uris": ["https://acme.example/callback"], + "terms_url": "https://acme.example/terms", + "privacy_url": "https://acme.example/privacy", + "description": None, + "created_at": "2025-06-01T10:00:00Z", + }, + ), + lambda c: c.account.applications.update( + "a1b2c3d4-5678-90ab-cdef-111111111111", + "b2c3d4e5-6789-01bc-defg-222222222222", + name="Acme Production App", + website_url="https://app.acme.example", + ), + lambda c: c.account.applications.update( + "a1b2c3d4-5678-90ab-cdef-111111111111", + "b2c3d4e5-6789-01bc-defg-222222222222", + name="Acme Production App", + website_url="https://app.acme.example", + ), + lambda r: isinstance(r, Application), + lambda r: isinstance(r, Application), + ) + + +def test_account_organizations_list() -> None: + """``GET /organizations/me/``""" + check_operation( + Expect( + method="GET", + path="/organizations/me/", + api="account", + auth="account_token", + status=200, + response_json=[ + { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "name": "developer", + "contact_email": "developer@gmail.com", + "created_at": "2025-06-01T10:00:00Z", + } + ], + ), + lambda c: c.account.organizations.list(), + lambda c: c.account.organizations.list(), + lambda r: isinstance(r, list) and all(isinstance(x1, Organization) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, Organization) for x1 in r), + ) diff --git a/tests/generated/test_age_estimation.py b/tests/generated/test_age_estimation.py new file mode 100644 index 0000000..749221b --- /dev/null +++ b/tests/generated/test_age_estimation.py @@ -0,0 +1,73 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``age_estimation`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.age_estimation.estimate_response import AgeEstimationEstimateResponse +from tests.harness import Expect, check_operation + + +def test_age_estimation_estimate() -> None: + """``POST /v3/age-estimation/``""" + check_operation( + Expect( + method="POST", + path="/v3/age-estimation/", + api="verification", + auth="api_key", + multipart_fields=[ + "age_estimation_decline_threshold", + "face_liveness_score_decline_threshold", + "metadata", + "rotate_image", + "save_api_request", + "vendor_data", + ], + multipart_files=["user_image"], + status=200, + response_json={ + "request_id": "0c40ba43-64ab-4e2e-b4b8-7d1f12f81bc1", + "age_estimation": { + "status": "Approved", + "method": "PASSIVE", + "score": 97.5, + "user_image": { + "entities": [ + { + "age": 27.33, + "bbox": [40, 40, 100, 100], + "confidence": 0.7177750468254089, + "gender": "male", + } + ], + "best_angle": 0, + }, + "age_estimation": 27.33, + "warnings": [], + }, + "vendor_data": "user-123", + "metadata": {"flow": "age-gate"}, + "created_at": "2026-06-12T02:24:11.512941+00:00", + }, + ), + lambda c: c.age_estimation.estimate( + user_image=b"file-content", + face_liveness_score_decline_threshold=30, + age_estimation_decline_threshold=18, + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "age-gate"}, + ), + lambda c: c.age_estimation.estimate( + user_image=b"file-content", + face_liveness_score_decline_threshold=30, + age_estimation_decline_threshold=18, + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "age-gate"}, + ), + lambda r: isinstance(r, AgeEstimationEstimateResponse), + lambda r: isinstance(r, AgeEstimationEstimateResponse), + ) diff --git a/tests/generated/test_aml.py b/tests/generated/test_aml.py new file mode 100644 index 0000000..bfc57e0 --- /dev/null +++ b/tests/generated/test_aml.py @@ -0,0 +1,45 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``aml`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.aml.screen_response import AmlScreenResponse +from tests.harness import Expect, check_operation + + +def test_aml_screen() -> None: + """``POST /v3/aml/``""" + check_operation( + Expect( + method="POST", + path="/v3/aml/", + api="verification", + auth="api_key", + json_body={"full_name": "Antonio Ejemplo Modelo"}, + status=200, + response_json={ + "request_id": "d27c5509-7a66-45a9-b05e-d69d9f4c336a", + "aml": { + "status": "Approved", + "total_hits": 0, + "entity_type": "person", + "hits": [], + "score": 0, + "screened_data": { + "full_name": "Maximilian Mustermann-Doe", + "date_of_birth": "1992-03-14", + "nationality": "DE", + "document_number": None, + }, + "warnings": [], + }, + "vendor_data": "user-1234", + "metadata": {"internal_ref": "abc-123"}, + "created_at": "2026-06-11T10:21:07.013938+00:00", + }, + ), + lambda c: c.aml.screen(full_name="Antonio Ejemplo Modelo"), + lambda c: c.aml.screen(full_name="Antonio Ejemplo Modelo"), + lambda r: isinstance(r, AmlScreenResponse), + lambda r: isinstance(r, AmlScreenResponse), + ) diff --git a/tests/generated/test_aml_monitoring.py b/tests/generated/test_aml_monitoring.py new file mode 100644 index 0000000..dc40f1f --- /dev/null +++ b/tests/generated/test_aml_monitoring.py @@ -0,0 +1,54 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``aml_monitoring`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.aml_monitoring.retrieve_job_response import AmlMonitoringRetrieveJobResponse +from tests.harness import Expect, check_operation + + +def test_aml_monitoring_retrieve_job() -> None: + """``GET /v3/aml-monitoring/jobs/{job_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/aml-monitoring/jobs/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "job_id": "00000000-0000-4000-8000-000000000002", + "status": "PENDING", + "is_enabled": True, + "target_type": "user", + "total": 1, + "processed": 1, + "outcomes": { + "enabled": 1, + "screened_and_enabled": 1, + "already_enabled": 1, + "disabled": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "skipped_sandbox": 1, + "failed_provider": 1, + "failed_insufficient_balance": 1, + }, + "results": [ + { + "didit_internal_id": "00000000-0000-4000-8000-000000000004", + "vendor_data": "sample", + "outcome": "enabled", + } + ], + "error": "sample", + "created_at": "2026-01-01T00:00:00Z", + "completed_at": "2026-01-01T00:00:00Z", + }, + strict_type=AmlMonitoringRetrieveJobResponse, + ), + lambda c: c.aml_monitoring.retrieve_job("00000000-0000-4000-8000-000000000001"), + lambda c: c.aml_monitoring.retrieve_job("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, AmlMonitoringRetrieveJobResponse), + lambda r: isinstance(r, AmlMonitoringRetrieveJobResponse), + ) diff --git a/tests/generated/test_billing.py b/tests/generated/test_billing.py new file mode 100644 index 0000000..397a91a --- /dev/null +++ b/tests/generated/test_billing.py @@ -0,0 +1,53 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``billing`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.billing.retrieve_balance_response import BillingRetrieveBalanceResponse +from didit.types.billing.top_up_response import BillingTopUpResponse +from tests.harness import Expect, check_operation + + +def test_billing_retrieve_balance() -> None: + """``GET /v3/billing/balance/``""" + check_operation( + Expect( + method="GET", + path="/v3/billing/balance/", + api="verification", + auth="api_key", + status=200, + response_json={ + "balance": "1234.5600", + "auto_refill_enabled": True, + "auto_refill_amount": "500.00", + "auto_refill_threshold": "100.00", + }, + ), + lambda c: c.billing.retrieve_balance(), + lambda c: c.billing.retrieve_balance(), + lambda r: isinstance(r, BillingRetrieveBalanceResponse), + lambda r: isinstance(r, BillingRetrieveBalanceResponse), + ) + + +def test_billing_top_up() -> None: + """``POST /v3/billing/top-up/``""" + check_operation( + Expect( + method="POST", + path="/v3/billing/top-up/", + api="verification", + auth="api_key", + json_body={"amount_in_dollars": 50}, + status=200, + response_json={ + "checkout_session_id": "cs_test_a1B2c3D4e5F6g7H8i9J0", + "checkout_session_url": "https://checkout.stripe.com/c/pay/cs_test_a1B2c3D4e5F6g7H8i9J0", + }, + ), + lambda c: c.billing.top_up(amount_in_dollars=50), + lambda c: c.billing.top_up(amount_in_dollars=50), + lambda r: isinstance(r, BillingTopUpResponse), + lambda r: isinstance(r, BillingTopUpResponse), + ) diff --git a/tests/generated/test_biometric_templates.py b/tests/generated/test_biometric_templates.py new file mode 100644 index 0000000..6cddced --- /dev/null +++ b/tests/generated/test_biometric_templates.py @@ -0,0 +1,227 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``biometric_templates`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.biometric_templates.batch_delete_response import ( + BiometricTemplatesBatchDeleteResponse, +) +from didit.types.biometric_templates.count_response import BiometricTemplatesCountResponse +from didit.types.biometric_templates.delete_response import BiometricTemplatesDeleteResponse +from didit.types.biometric_templates.list_response import BiometricTemplatesListResponseResults +from didit.types.biometric_templates.retrieve_response import BiometricTemplatesRetrieveResponse +from tests.harness import Expect, check_operation + + +def test_biometric_templates_batch_delete() -> None: + """``POST /v3/biometric-templates/delete/``""" + check_operation( + Expect( + method="POST", + path="/v3/biometric-templates/delete/", + api="verification", + auth="api_key", + json_body={ + "template_uuids": [ + "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + ] + }, + status=200, + response_json={ + "results": [ + {"uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", "outcome": "purged"}, + {"uuid": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "outcome": "already_purged"}, + ] + }, + ), + lambda c: c.biometric_templates.batch_delete( + template_uuids=[ + "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + ] + ), + lambda c: c.biometric_templates.batch_delete( + template_uuids=[ + "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", + ] + ), + lambda r: isinstance(r, BiometricTemplatesBatchDeleteResponse), + lambda r: isinstance(r, BiometricTemplatesBatchDeleteResponse), + ) + + +def test_biometric_templates_count() -> None: + """``GET /v3/biometric-templates/count/``""" + check_operation( + Expect( + method="GET", + path="/v3/biometric-templates/count/", + api="verification", + auth="api_key", + query={ + "vendor_data": ["sample"], + "vendor_user_uuid": ["00000000-0000-4000-8000-000000000001"], + "status": ["pending"], + "source_type": ["session_delete"], + "retained_from": ["2026-01-01T00:00:00Z"], + "retained_to": ["2026-01-01T00:00:00Z"], + }, + status=200, + response_json={"count": 128}, + ), + lambda c: c.biometric_templates.count( + vendor_data="sample", + vendor_user_uuid="00000000-0000-4000-8000-000000000001", + status="pending", + source_type="session_delete", + retained_from="2026-01-01T00:00:00Z", + retained_to="2026-01-01T00:00:00Z", + ), + lambda c: c.biometric_templates.count( + vendor_data="sample", + vendor_user_uuid="00000000-0000-4000-8000-000000000001", + status="pending", + source_type="session_delete", + retained_from="2026-01-01T00:00:00Z", + retained_to="2026-01-01T00:00:00Z", + ), + lambda r: isinstance(r, BiometricTemplatesCountResponse), + lambda r: isinstance(r, BiometricTemplatesCountResponse), + ) + + +def test_biometric_templates_delete() -> None: + """``DELETE /v3/biometric-templates/{template_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/biometric-templates/9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b/", + api="verification", + auth="api_key", + status=200, + response_json={"uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", "outcome": "purged"}, + ), + lambda c: c.biometric_templates.delete("9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b"), + lambda c: c.biometric_templates.delete("9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b"), + lambda r: isinstance(r, BiometricTemplatesDeleteResponse), + lambda r: isinstance(r, BiometricTemplatesDeleteResponse), + ) + + +def test_biometric_templates_list() -> None: + """``GET /v3/biometric-templates/``""" + check_operation( + Expect( + method="GET", + path="/v3/biometric-templates/", + api="verification", + auth="api_key", + query={ + "vendor_data": ["sample"], + "vendor_user_uuid": ["00000000-0000-4000-8000-000000000001"], + "status": ["pending"], + "source_type": ["session_delete"], + "retained_from": ["2026-01-01T00:00:00Z"], + "retained_to": ["2026-01-01T00:00:00Z"], + "ordering": ["retained_at"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "vendor_data": "user-42", + "source_type": "session_delete", + "provenance_reference": "6b1e8d2f-4a3c-4f9e-8c7d-1a2b3c4d5e6f", + "retention_policy": "retain_until_user_deleted", + "retention_override": None, + "retained_at": "2026-08-28T10:15:42.318402Z", + "expires_at": "2027-08-28T10:15:42.318402Z", + "retained_by": "3c9e2b1f-7d4a-4e8b-9c1d-5f6a7b8c9d0e", + "retained_by_type": "API_KEY", + "instruction_class": "operational_session_delete", + "instruction_source": "api", + "instruction_id": "post-approval-cleanup-2026-08", + "held_out_of_use": False, + "status": "active", + "purged_at": None, + } + ], + }, + ), + lambda c: c.biometric_templates.list( + vendor_data="sample", + vendor_user_uuid="00000000-0000-4000-8000-000000000001", + status="pending", + source_type="session_delete", + retained_from="2026-01-01T00:00:00Z", + retained_to="2026-01-01T00:00:00Z", + ordering="retained_at", + limit=1, + offset=1, + ), + lambda c: c.biometric_templates.list( + vendor_data="sample", + vendor_user_uuid="00000000-0000-4000-8000-000000000001", + status="pending", + source_type="session_delete", + retained_from="2026-01-01T00:00:00Z", + retained_to="2026-01-01T00:00:00Z", + ordering="retained_at", + limit=1, + offset=1, + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, BiometricTemplatesListResponseResults) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, BiometricTemplatesListResponseResults) for x1 in r.results) + ), + ) + + +def test_biometric_templates_retrieve() -> None: + """``GET /v3/biometric-templates/{template_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/biometric-templates/9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "vendor_user_uuid": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "vendor_data": "user-42", + "source_type": "session_delete", + "provenance_reference": "6b1e8d2f-4a3c-4f9e-8c7d-1a2b3c4d5e6f", + "retention_policy": "retain_until_user_deleted", + "retention_override": None, + "retained_at": "2026-08-28T10:15:42.318402Z", + "expires_at": "2027-08-28T10:15:42.318402Z", + "retained_by": "3c9e2b1f-7d4a-4e8b-9c1d-5f6a7b8c9d0e", + "retained_by_type": "API_KEY", + "instruction_class": "operational_session_delete", + "instruction_source": "api", + "instruction_id": "post-approval-cleanup-2026-08", + "held_out_of_use": False, + "status": "active", + "purged_at": None, + }, + ), + lambda c: c.biometric_templates.retrieve("9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b"), + lambda c: c.biometric_templates.retrieve("9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b"), + lambda r: isinstance(r, BiometricTemplatesRetrieveResponse), + lambda r: isinstance(r, BiometricTemplatesRetrieveResponse), + ) diff --git a/tests/generated/test_business_sessions.py b/tests/generated/test_business_sessions.py new file mode 100644 index 0000000..8db98ed --- /dev/null +++ b/tests/generated/test_business_sessions.py @@ -0,0 +1,52 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``business_sessions`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.business_sessions.list_networks_response import ( + BusinessSessionsListNetworksResponse, +) +from tests.harness import Expect, check_operation + + +def test_business_sessions_list_networks() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/business-sessions/{business_session_id}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/business-sessions/00000000-0000-4000-8000-000000000001/networks/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "network": { + "uuid": "00000000-0000-4000-8000-000000000007", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + }, + "subject": {}, + "signal_types": ["ip_address"], + "signal_count": 1, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=BusinessSessionsListNetworksResponse, + ), + lambda c: c.business_sessions.list_networks("00000000-0000-4000-8000-000000000001"), + lambda c: c.business_sessions.list_networks("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, BusinessSessionsListNetworksResponse), + lambda r: isinstance(r, BusinessSessionsListNetworksResponse), + ) diff --git a/tests/generated/test_businesses.py b/tests/generated/test_businesses.py new file mode 100644 index 0000000..712a94a --- /dev/null +++ b/tests/generated/test_businesses.py @@ -0,0 +1,563 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``businesses`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.businesses.batch_delete_response import BusinessesBatchDeleteResponse +from didit.types.businesses.list_networks_response import BusinessesListNetworksResponse +from didit.types.businesses.retrieve_ongoing_monitoring_job_response import ( + BusinessesRetrieveOngoingMonitoringJobResponse, +) +from didit.types.businesses.retrieve_session_ongoing_monitoring_job_response import ( + BusinessesRetrieveSessionOngoingMonitoringJobResponse, +) +from didit.types.businesses.set_aml_monitoring_response import ( + BusinessesSetAmlMonitoringResponse202, + BusinessesSetAmlMonitoringResponseVariant1, + BusinessesSetAmlMonitoringResponseVariant2, +) +from didit.types.businesses.set_ongoing_monitoring_response import ( + BusinessesSetOngoingMonitoringResponse202, + BusinessesSetOngoingMonitoringResponseVariant1, + BusinessesSetOngoingMonitoringResponseVariant2, +) +from didit.types.businesses.set_session_ongoing_monitoring_response import ( + BusinessesSetSessionOngoingMonitoringResponse202, + BusinessesSetSessionOngoingMonitoringResponseVariant1, + BusinessesSetSessionOngoingMonitoringResponseVariant2, +) +from didit.types.components.business_detail_item import BusinessDetailItem +from didit.types.components.business_list_item import BusinessListItem +from tests.harness import Expect, check_operation + + +def test_businesses_batch_delete() -> None: + """``POST /v3/businesses/delete/``""" + check_operation( + Expect( + method="POST", + path="/v3/businesses/delete/", + api="verification", + auth="api_key", + json_body={"vendor_data_list": ["company-123", "company-456"]}, + status=200, + response_json={"deleted": 2}, + ), + lambda c: c.businesses.batch_delete(vendor_data_list=["company-123", "company-456"]), + lambda c: c.businesses.batch_delete(vendor_data_list=["company-123", "company-456"]), + lambda r: isinstance(r, BusinessesBatchDeleteResponse), + lambda r: isinstance(r, BusinessesBatchDeleteResponse), + ) + + +def test_businesses_create() -> None: + """``POST /v3/businesses/create/``""" + check_operation( + Expect( + method="POST", + path="/v3/businesses/create/", + api="verification", + auth="api_key", + json_body={ + "vendor_data": "company-456", + "legal_name": "New Corp Ltd", + "country_code": "US", + }, + status=201, + response_json={ + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-456", + "display_name": None, + "legal_name": "New Corp Ltd", + "registration_number": None, + "country_code": "US", + "effective_name": "New Corp Ltd", + "status": "ACTIVE", + "session_count": 0, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 0, + "features": {}, + "features_list": [], + "last_session_at": None, + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": None, + "tags": [], + "created_at": "2026-03-15T10:30:00Z", + "metadata": {}, + "updated_at": "2026-03-15T10:30:00Z", + }, + ), + lambda c: c.businesses.create( + vendor_data="company-456", legal_name="New Corp Ltd", country_code="US" + ), + lambda c: c.businesses.create( + vendor_data="company-456", legal_name="New Corp Ltd", country_code="US" + ), + lambda r: isinstance(r, BusinessDetailItem), + lambda r: isinstance(r, BusinessDetailItem), + ) + + +def test_businesses_list() -> None: + """``GET /v3/businesses/``""" + check_operation( + Expect( + method="GET", + path="/v3/businesses/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": None, + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Trading Ltd", + "status": "ACTIVE", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": {"KYB": "Approved"}, + "features_list": [{"feature": "KYB", "status": "Approved"}], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF", + } + ], + "created_at": "2026-01-10T08:00:00Z", + } + ], + }, + ), + lambda c: c.businesses.list(limit=1, offset=1), + lambda c: c.businesses.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, BusinessListItem) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, BusinessListItem) for x1 in r.results) + ), + ) + + +def test_businesses_list_networks() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/businesses/{vendor_business_uuid}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/businesses/00000000-0000-4000-8000-000000000001/networks/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "network": { + "uuid": "00000000-0000-4000-8000-000000000007", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + }, + "subject": {}, + "signal_types": ["ip_address"], + "signal_count": 1, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=BusinessesListNetworksResponse, + ), + lambda c: c.businesses.list_networks("00000000-0000-4000-8000-000000000001"), + lambda c: c.businesses.list_networks("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, BusinessesListNetworksResponse), + lambda r: isinstance(r, BusinessesListNetworksResponse), + ) + + +def test_businesses_retrieve() -> None: + """``GET /v3/businesses/{vendor_data}/``""" + check_operation( + Expect( + method="GET", + path="/v3/businesses/company-123/", + api="verification", + auth="api_key", + status=200, + response_json={ + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": None, + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Trading Ltd", + "status": "ACTIVE", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": {"KYB": "Approved"}, + "features_list": [{"feature": "KYB", "status": "Approved"}], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF", + } + ], + "created_at": "2026-01-10T08:00:00Z", + "metadata": {"industry": "fintech"}, + "updated_at": "2026-03-15T10:30:00Z", + }, + ), + lambda c: c.businesses.retrieve("company-123"), + lambda c: c.businesses.retrieve("company-123"), + lambda r: isinstance(r, BusinessDetailItem), + lambda r: isinstance(r, BusinessDetailItem), + ) + + +def test_businesses_retrieve_ongoing_monitoring_job() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/{job_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/vendor-businesses/toggle-ongoing-monitoring/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "job_id": "00000000-0000-4000-8000-000000000002", + "status": "PENDING", + "is_enabled": True, + "total": 1, + "processed": 1, + "outcomes": { + "enabled": 1, + "screened_and_enabled": 1, + "already_enabled": 1, + "disabled": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "skipped_sandbox": 1, + "failed_provider": 1, + "failed_insufficient_balance": 1, + }, + "error": "sample", + "estimate": { + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + }, + }, + strict_type=BusinessesRetrieveOngoingMonitoringJobResponse, + ), + lambda c: c.businesses.retrieve_ongoing_monitoring_job( + "00000000-0000-4000-8000-000000000001" + ), + lambda c: c.businesses.retrieve_ongoing_monitoring_job( + "00000000-0000-4000-8000-000000000001" + ), + lambda r: isinstance(r, BusinessesRetrieveOngoingMonitoringJobResponse), + lambda r: isinstance(r, BusinessesRetrieveOngoingMonitoringJobResponse), + ) + + +def test_businesses_retrieve_session_ongoing_monitoring_job() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/{job_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/businesses/toggle-ongoing-monitoring/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "job_id": "00000000-0000-4000-8000-000000000002", + "status": "PENDING", + "is_enabled": True, + "total": 1, + "processed": 1, + "outcomes": { + "enabled": 1, + "screened_and_enabled": 1, + "already_enabled": 1, + "disabled": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "skipped_sandbox": 1, + "failed_provider": 1, + "failed_insufficient_balance": 1, + }, + "error": "sample", + "estimate": { + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + }, + }, + strict_type=BusinessesRetrieveSessionOngoingMonitoringJobResponse, + ), + lambda c: c.businesses.retrieve_session_ongoing_monitoring_job( + "00000000-0000-4000-8000-000000000001" + ), + lambda c: c.businesses.retrieve_session_ongoing_monitoring_job( + "00000000-0000-4000-8000-000000000001" + ), + lambda r: isinstance(r, BusinessesRetrieveSessionOngoingMonitoringJobResponse), + lambda r: isinstance(r, BusinessesRetrieveSessionOngoingMonitoringJobResponse), + ) + + +def test_businesses_set_aml_monitoring() -> None: + """``POST /v3/businesses/aml-monitoring/``""" + check_operation( + Expect( + method="POST", + path="/v3/businesses/aml-monitoring/", + api="verification", + auth="api_key", + json_body={ + "is_enabled": True, + "estimate_only": True, + "vendor_data_list": ["company-123", "company-456"], + }, + status=200, + response_json={ + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + "unmatched": ["sample"], + }, + ), + lambda c: c.businesses.set_aml_monitoring( + is_enabled=True, estimate_only=True, vendor_data_list=["company-123", "company-456"] + ), + lambda c: c.businesses.set_aml_monitoring( + is_enabled=True, estimate_only=True, vendor_data_list=["company-123", "company-456"] + ), + lambda r: ( + (isinstance(r, BusinessesSetAmlMonitoringResponseVariant1)) + or (isinstance(r, BusinessesSetAmlMonitoringResponseVariant2)) + or (isinstance(r, BusinessesSetAmlMonitoringResponse202)) + ), + lambda r: ( + (isinstance(r, BusinessesSetAmlMonitoringResponseVariant1)) + or (isinstance(r, BusinessesSetAmlMonitoringResponseVariant2)) + or (isinstance(r, BusinessesSetAmlMonitoringResponse202)) + ), + ) + + +def test_businesses_set_ongoing_monitoring() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-businesses/toggle-ongoing-monitoring/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/vendor-businesses/toggle-ongoing-monitoring/", + api="verification", + auth="api_key", + json_body={"is_enabled": True}, + status=200, + response_json={ + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + }, + ), + lambda c: c.businesses.set_ongoing_monitoring(is_enabled=True), + lambda c: c.businesses.set_ongoing_monitoring(is_enabled=True), + lambda r: ( + (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant1)) + or (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant2)) + or (isinstance(r, BusinessesSetOngoingMonitoringResponse202)) + ), + lambda r: ( + (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant1)) + or (isinstance(r, BusinessesSetOngoingMonitoringResponseVariant2)) + or (isinstance(r, BusinessesSetOngoingMonitoringResponse202)) + ), + ) + + +def test_businesses_set_session_ongoing_monitoring() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/businesses/toggle-ongoing-monitoring/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/businesses/toggle-ongoing-monitoring/", + api="verification", + auth="api_key", + json_body={"is_enabled": True}, + status=200, + response_json={ + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + }, + ), + lambda c: c.businesses.set_session_ongoing_monitoring(is_enabled=True), + lambda c: c.businesses.set_session_ongoing_monitoring(is_enabled=True), + lambda r: ( + (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant1)) + or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant2)) + or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponse202)) + ), + lambda r: ( + (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant1)) + or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponseVariant2)) + or (isinstance(r, BusinessesSetSessionOngoingMonitoringResponse202)) + ), + ) + + +def test_businesses_update() -> None: + """``PATCH /v3/businesses/{vendor_data}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/businesses/company-123/", + api="verification", + auth="api_key", + json_body={"display_name": "Acme Corp"}, + status=200, + response_json={ + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": "Acme Corp", + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Corp", + "status": "FLAGGED", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": {"KYB": "Approved"}, + "features_list": [{"feature": "KYB", "status": "Approved"}], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF", + } + ], + "created_at": "2026-01-10T08:00:00Z", + "metadata": {"industry": "fintech", "risk_level": "low"}, + "updated_at": "2026-03-15T10:30:00Z", + }, + ), + lambda c: c.businesses.update("company-123", display_name="Acme Corp"), + lambda c: c.businesses.update("company-123", display_name="Acme Corp"), + lambda r: isinstance(r, BusinessDetailItem), + lambda r: isinstance(r, BusinessDetailItem), + ) + + +def test_businesses_update_status() -> None: + """``PATCH /v3/businesses/{vendor_data}/update-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/businesses/company-123/update-status/", + api="verification", + auth="api_key", + json_body={"status": "FLAGGED"}, + status=200, + response_json={ + "didit_internal_id": "a1b2c3d4-e5f6-4890-abcd-ef1234567890", + "vendor_data": "company-123", + "display_name": None, + "legal_name": "Acme Trading Ltd", + "registration_number": "12345678", + "country_code": "GB", + "effective_name": "Acme Trading Ltd", + "status": "BLOCKED", + "session_count": 2, + "approved_count": 1, + "declined_count": 0, + "in_review_count": 1, + "features": {"KYB": "Approved"}, + "features_list": [{"feature": "KYB", "status": "Approved"}], + "last_session_at": "2026-03-15T10:30:00Z", + "last_activity_at": "2026-03-15T10:30:00Z", + "first_session_at": "2026-01-10T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF", + } + ], + "created_at": "2026-01-10T08:00:00Z", + "metadata": {"industry": "fintech"}, + "updated_at": "2026-03-15T10:30:00Z", + }, + ), + lambda c: c.businesses.update_status("company-123", status="FLAGGED"), + lambda c: c.businesses.update_status("company-123", status="FLAGGED"), + lambda r: isinstance(r, BusinessDetailItem), + lambda r: isinstance(r, BusinessDetailItem), + ) diff --git a/tests/generated/test_cases.py b/tests/generated/test_cases.py new file mode 100644 index 0000000..f0d9c46 --- /dev/null +++ b/tests/generated/test_cases.py @@ -0,0 +1,4129 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``cases`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.cases.blueprints.install_presets_response import ( + CasesBlueprintsInstallPresetsResponse, +) +from didit.types.cases.blueprints.list_presets_response import CasesBlueprintsListPresetsResponse +from didit.types.cases.bulk_assign_response import CasesBulkAssignResponse +from didit.types.cases.bulk_update_status_response import CasesBulkUpdateStatusResponse +from didit.types.cases.list_reviewers_response import CasesListReviewersResponse +from didit.types.cases.notes.create_attachment_upload_response import ( + CasesNotesCreateAttachmentUploadResponse, +) +from didit.types.cases.notes.retrieve_attachment_url_response import ( + CasesNotesRetrieveAttachmentUrlResponse, +) +from didit.types.cases.print_response import CasesPrintResponse +from didit.types.cases.retrieve_statistics_response import CasesRetrieveStatisticsResponse +from didit.types.components.case_action_case_action_receipt import CaseActionCaseActionReceipt +from didit.types.components.case_action_case_action_receipt_page import ( + CaseActionCaseActionReceiptPage, +) +from didit.types.components.case_action_case_entity_action_result import ( + CaseActionCaseEntityActionResult, +) +from didit.types.components.case_blueprint_detail import CaseBlueprintDetail +from didit.types.components.case_blueprint_list_item import CaseBlueprintListItem +from didit.types.components.case_checklist_item import CaseChecklistItem +from didit.types.components.case_collaboration_case_note import CaseCollaborationCaseNote +from didit.types.components.case_detail import CaseDetail +from didit.types.components.case_duplicate_case_duplicate_page import CaseDuplicateCaseDuplicatePage +from didit.types.components.case_event import CaseEvent +from didit.types.components.case_link import CaseLink +from didit.types.components.case_list_item import CaseListItem +from didit.types.components.case_publication_case_blueprint import CasePublicationCaseBlueprint +from didit.types.components.case_publication_policy_draft import CasePublicationPolicyDraft +from didit.types.components.case_publication_policy_draft_response import ( + CasePublicationPolicyDraftResponse, +) +from didit.types.components.case_publication_policy_revision import CasePublicationPolicyRevision +from didit.types.components.case_review_team import CaseReviewTeam +from didit.types.components.case_routing_preview import CaseRoutingPreview +from didit.types.components.case_routing_rule import CaseRoutingRule +from didit.types.components.customer_messaging_case_request import CustomerMessagingCaseRequest +from didit.types.components.customer_messaging_case_request_options import ( + CustomerMessagingCaseRequestOptions, +) +from didit.types.components.customer_messaging_case_request_preview_response import ( + CustomerMessagingCaseRequestPreviewResponse, +) +from didit.types.components.regulatory_report_detail import RegulatoryReportDetail +from didit.types.components.routing_publication_case_routing_rule import ( + RoutingPublicationCaseRoutingRule, +) +from didit.types.components.routing_publication_policy_draft import RoutingPublicationPolicyDraft +from didit.types.components.routing_publication_policy_draft_response import ( + RoutingPublicationPolicyDraftResponse, +) +from didit.types.components.routing_publication_policy_revision import ( + RoutingPublicationPolicyRevision, +) +from tests.harness import BINARY_BODY, Expect, check_operation + + +def test_cases_apply_entity_action() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/entity-actions/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/entity-actions/", + api="verification", + auth="api_key", + json_body={"action": "set_status"}, + status=200, + response_json={ + "receipt": { + "uuid": "00000000-0000-4000-8000-000000000005", + "client_reference": "00000000-0000-4000-8000-000000000005", + "action": "sample", + "status": "APPLIED", + "result": "value", + "error": "sample", + "actor_email": "user@example.com", + "attempts": 1, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + "entity_type": "user", + "entity_uuid": "00000000-0000-4000-8000-000000000003", + "action": "sample", + "previous_status": "sample", + "new_status": "sample", + "tags_added": ["sample"], + "comment_uuid": "00000000-0000-4000-8000-000000000003", + "blocklisted": ["sample"], + }, + strict_type=CaseActionCaseEntityActionResult, + ), + lambda c: c.cases.apply_entity_action( + "00000000-0000-4000-8000-000000000001", action="set_status" + ), + lambda c: c.cases.apply_entity_action( + "00000000-0000-4000-8000-000000000001", action="set_status" + ), + lambda r: isinstance(r, CaseActionCaseEntityActionResult), + lambda r: isinstance(r, CaseActionCaseEntityActionResult), + ) + + +def test_cases_approve_resolution() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/approve/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/approve/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.approve_resolution("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.approve_resolution("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_assign() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/assign/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/assign/", + api="verification", + auth="api_key", + json_body={"assignee_user_id": "00000000-0000-4000-8000-000000000001"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.assign( + "c1a5e000-0000-4000-8000-000000000001", + assignee_user_id="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.assign( + "c1a5e000-0000-4000-8000-000000000001", + assignee_user_id="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_bulk_assign() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/assign/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/bulk/assign/", + api="verification", + auth="api_key", + json_body={ + "case_uuids": ["00000000-0000-4000-8000-000000000002"], + "assignee_user_id": "00000000-0000-4000-8000-000000000001", + }, + status=200, + response_json={ + "updated": 1, + "failures": [ + {"case_uuid": "00000000-0000-4000-8000-000000000004", "detail": "sample"} + ], + }, + strict_type=CasesBulkAssignResponse, + ), + lambda c: c.cases.bulk_assign( + case_uuids=["00000000-0000-4000-8000-000000000002"], + assignee_user_id="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.bulk_assign( + case_uuids=["00000000-0000-4000-8000-000000000002"], + assignee_user_id="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, CasesBulkAssignResponse), + lambda r: isinstance(r, CasesBulkAssignResponse), + ) + + +def test_cases_bulk_update_status() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/bulk/status/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/bulk/status/", + api="verification", + auth="api_key", + json_body={"case_uuids": ["00000000-0000-4000-8000-000000000002"], "status": "OPEN"}, + status=200, + response_json={ + "updated": 1, + "failures": [ + {"case_uuid": "00000000-0000-4000-8000-000000000004", "detail": "sample"} + ], + }, + strict_type=CasesBulkUpdateStatusResponse, + ), + lambda c: c.cases.bulk_update_status( + case_uuids=["00000000-0000-4000-8000-000000000002"], status="OPEN" + ), + lambda c: c.cases.bulk_update_status( + case_uuids=["00000000-0000-4000-8000-000000000002"], status="OPEN" + ), + lambda r: isinstance(r, CasesBulkUpdateStatusResponse), + lambda r: isinstance(r, CasesBulkUpdateStatusResponse), + ) + + +def test_cases_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/", + api="verification", + auth="api_key", + json_body={"title": "sample"}, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.create(title="sample"), + lambda c: c.cases.create(title="sample"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.cases.delete("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.delete("c1a5e000-0000-4000-8000-000000000001"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_cases_escalate() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/escalate/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/escalate/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.escalate("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.escalate("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_find_duplicates() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/duplicates/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/duplicates/", + api="verification", + auth="api_key", + json_body={"subject": {"type": "user", "uuid": "00000000-0000-4000-8000-000000000003"}}, + status=200, + response_json={ + "count": 1, + "next_offset": 1, + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "priority": "LOW", + "created_at": "2026-01-01T00:00:00Z", + } + ], + }, + strict_type=CaseDuplicateCaseDuplicatePage, + ), + lambda c: c.cases.find_duplicates( + subject={"type": "user", "uuid": "00000000-0000-4000-8000-000000000003"} + ), + lambda c: c.cases.find_duplicates( + subject={"type": "user", "uuid": "00000000-0000-4000-8000-000000000003"} + ), + lambda r: isinstance(r, CaseDuplicateCaseDuplicatePage), + lambda r: isinstance(r, CaseDuplicateCaseDuplicatePage), + ) + + +def test_cases_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/", + api="verification", + auth="api_key", + query={ + "limit": ["1"], + "offset": ["1"], + "status__in": ["sample"], + "priority": ["sample"], + "source__in": ["sample"], + "blueprint": ["00000000-0000-4000-8000-000000000001"], + "assigned_to": ["00000000-0000-4000-8000-000000000001"], + "unassigned": ["true"], + "tag": ["sample"], + "search": ["sample"], + "date_from": ["2026-01-01"], + "date_to": ["2026-01-01"], + "due_before": ["2026-01-01T00:00:00Z"], + "overdue": ["true"], + "ordering": ["created_at"], + }, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000007", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000007", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000007", + "name": "sample", + }, + "links_count": 1, + "notes_count": 1, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "application_id": "00000000-0000-4000-8000-000000000005", + "application_name": "sample", + "organization_id": "00000000-0000-4000-8000-000000000005", + "organization_name": "sample", + } + ], + }, + ), + lambda c: c.cases.list( + limit=1, + offset=1, + status__in="sample", + priority="sample", + source__in="sample", + blueprint="00000000-0000-4000-8000-000000000001", + assigned_to="00000000-0000-4000-8000-000000000001", + unassigned=True, + tag="sample", + search="sample", + date_from="2026-01-01", + date_to="2026-01-01", + due_before="2026-01-01T00:00:00Z", + overdue=True, + ordering="created_at", + ), + lambda c: c.cases.list( + limit=1, + offset=1, + status__in="sample", + priority="sample", + source__in="sample", + blueprint="00000000-0000-4000-8000-000000000001", + assigned_to="00000000-0000-4000-8000-000000000001", + unassigned=True, + tag="sample", + search="sample", + date_from="2026-01-01", + date_to="2026-01-01", + due_before="2026-01-01T00:00:00Z", + overdue=True, + ordering="created_at", + ), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, CaseListItem) for x1 in r.results), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, CaseListItem) for x1 in r.results) + ), + ) + + +def test_cases_list_action_receipts() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/action-receipts/", + api="verification", + auth="api_key", + query={"offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next_offset": 1, + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "client_reference": "00000000-0000-4000-8000-000000000006", + "action": "sample", + "status": "APPLIED", + "result": "value", + "error": "sample", + "actor_email": "user@example.com", + "attempts": 1, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ], + }, + strict_type=CaseActionCaseActionReceiptPage, + ), + lambda c: c.cases.list_action_receipts("00000000-0000-4000-8000-000000000001", offset=1), + lambda c: c.cases.list_action_receipts("00000000-0000-4000-8000-000000000001", offset=1), + lambda r: isinstance(r, CaseActionCaseActionReceiptPage), + lambda r: isinstance(r, CaseActionCaseActionReceiptPage), + ) + + +def test_cases_list_aml_hits() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/aml/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/aml/", + api="verification", + auth="api_key", + status=200, + response_json=[{}], + strict_type=list[dict[str, object]], + ), + lambda c: c.cases.list_aml_hits("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.list_aml_hits("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, list) and all(isinstance(x1, dict) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, dict) for x1 in r), + ) + + +def test_cases_list_events() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/events/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/events/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.cases.list_events("c1a5e000-0000-4000-8000-000000000001", limit=1, offset=1), + lambda c: c.cases.list_events("c1a5e000-0000-4000-8000-000000000001", limit=1, offset=1), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, CaseEvent) for x1 in r.results), + lambda r: isinstance(r, AsyncPage) and all(isinstance(x1, CaseEvent) for x1 in r.results), + ) + + +def test_cases_list_reviewers() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/reviewers/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/reviewers/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "user_id": "00000000-0000-4000-8000-000000000004", + "email": "user@example.com", + "name": "sample", + } + ] + }, + strict_type=CasesListReviewersResponse, + ), + lambda c: c.cases.list_reviewers(), + lambda c: c.cases.list_reviewers(), + lambda r: isinstance(r, CasesListReviewersResponse), + lambda r: isinstance(r, CasesListReviewersResponse), + ) + + +def test_cases_print() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/print/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/print/", + api="verification", + auth="api_key", + status=200, + response_json={"url": "https://example.com/resource"}, + strict_type=CasesPrintResponse, + ), + lambda c: c.cases.print("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.print("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CasesPrintResponse), + lambda r: isinstance(r, CasesPrintResponse), + ) + + +def test_cases_reject_resolution() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reject-approval/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/reject-approval/", + api="verification", + auth="api_key", + json_body={"notes": "sample"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.reject_resolution("c1a5e000-0000-4000-8000-000000000001", notes="sample"), + lambda c: c.cases.reject_resolution("c1a5e000-0000-4000-8000-000000000001", notes="sample"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_reopen() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/reopen/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/reopen/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.reopen("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.reopen("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_request_info() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/request-info/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/request-info/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.request_info("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.request_info("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_resolve() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/resolve/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/resolve/", + api="verification", + auth="api_key", + json_body={"resolution": "FALSE_POSITIVE"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.resolve( + "c1a5e000-0000-4000-8000-000000000001", resolution="FALSE_POSITIVE" + ), + lambda c: c.cases.resolve( + "c1a5e000-0000-4000-8000-000000000001", resolution="FALSE_POSITIVE" + ), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.retrieve("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.retrieve("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_retrieve_statistics() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/statistics/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/statistics/", + api="verification", + auth="api_key", + query={ + "date_from": ["2026-01-01"], + "date_to": ["2026-01-01"], + "blueprint": ["00000000-0000-4000-8000-000000000001"], + "assigned_to": ["00000000-0000-4000-8000-000000000001"], + "priority": ["LOW"], + "source": ["MANUAL"], + "status": ["OPEN"], + }, + status=200, + response_json={ + "series": [{"date": "2026-01-01", "created": 1, "reviewed": 1}], + "tables": { + "officer": [ + { + "key": "sample", + "created": 1, + "assigned": 1, + "resolved": 1, + "threat_rate": 1.5, + "false_positive_rate": 1.5, + "unresolved": 1, + "reassignment_rate": 1.5, + "escalation_rate": 1.5, + "avg_assign_hours": 1.5, + "avg_resolve_hours": 1.5, + "avg_handling_hours": 1.5, + } + ], + "blueprint": [ + { + "key": "sample", + "created": 1, + "assigned": 1, + "resolved": 1, + "threat_rate": 1.5, + "false_positive_rate": 1.5, + "unresolved": 1, + "reassignment_rate": 1.5, + "escalation_rate": 1.5, + "avg_assign_hours": 1.5, + "avg_resolve_hours": 1.5, + "avg_handling_hours": 1.5, + } + ], + "source": [ + { + "key": "sample", + "created": 1, + "assigned": 1, + "resolved": 1, + "threat_rate": 1.5, + "false_positive_rate": 1.5, + "unresolved": 1, + "reassignment_rate": 1.5, + "escalation_rate": 1.5, + "avg_assign_hours": 1.5, + "avg_resolve_hours": 1.5, + "avg_handling_hours": 1.5, + } + ], + "status": [ + { + "key": "sample", + "created": 1, + "assigned": 1, + "resolved": 1, + "threat_rate": 1.5, + "false_positive_rate": 1.5, + "unresolved": 1, + "reassignment_rate": 1.5, + "escalation_rate": 1.5, + "avg_assign_hours": 1.5, + "avg_resolve_hours": 1.5, + "avg_handling_hours": 1.5, + } + ], + }, + "totals": { + "created": 1, + "assigned": 1, + "resolved": 1, + "threat_rate": 1.5, + "false_positive_rate": 1.5, + "unresolved": 1, + "reassignment_rate": 1.5, + "escalation_rate": 1.5, + "avg_assign_hours": 1.5, + "avg_resolve_hours": 1.5, + "avg_handling_hours": 1.5, + }, + "queue": {"open": 1, "overdue": 1}, + }, + strict_type=CasesRetrieveStatisticsResponse, + ), + lambda c: c.cases.retrieve_statistics( + date_from="2026-01-01", + date_to="2026-01-01", + blueprint="00000000-0000-4000-8000-000000000001", + assigned_to="00000000-0000-4000-8000-000000000001", + priority="LOW", + source="MANUAL", + status="OPEN", + ), + lambda c: c.cases.retrieve_statistics( + date_from="2026-01-01", + date_to="2026-01-01", + blueprint="00000000-0000-4000-8000-000000000001", + assigned_to="00000000-0000-4000-8000-000000000001", + priority="LOW", + source="MANUAL", + status="OPEN", + ), + lambda r: isinstance(r, CasesRetrieveStatisticsResponse), + lambda r: isinstance(r, CasesRetrieveStatisticsResponse), + ) + + +def test_cases_retry_action_receipt() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/action-receipts/{receipt_uuid}/retry/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/action-receipts/00000000-0000-4000-8000-000000000002/retry/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "client_reference": "00000000-0000-4000-8000-000000000003", + "action": "sample", + "status": "APPLIED", + "result": "value", + "error": "sample", + "actor_email": "user@example.com", + "attempts": 1, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseActionCaseActionReceipt, + ), + lambda c: c.cases.retry_action_receipt( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.cases.retry_action_receipt( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: isinstance(r, CaseActionCaseActionReceipt), + lambda r: isinstance(r, CaseActionCaseActionReceipt), + ) + + +def test_cases_transfer() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/transfer/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/transfer/", + api="verification", + auth="api_key", + json_body={"target_blueprint": "00000000-0000-4000-8000-000000000001"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.transfer( + "c1a5e000-0000-4000-8000-000000000001", + target_blueprint="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.transfer( + "c1a5e000-0000-4000-8000-000000000001", + target_blueprint="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"title": "sample"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "title": "sample", + "status": "OPEN", + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "priority": "LOW", + "source": "MANUAL", + "tags": ["sample"], + "subject": { + "type": "user", + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "external_id": "sample", + }, + "assigned_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "full_name": "sample", + "email": "user@example.com", + }, + "assigned_at": "2026-01-01T00:00:00Z", + "due_at": "2026-01-01T00:00:00Z", + "resolved_at": "2026-01-01T00:00:00Z", + "resolved_by_email": "sample", + "pending_resolution": { + "resolution": "FALSE_POSITIVE", + "resolution_notes": "sample", + "maker_email": "user@example.com", + "maker_name": "sample", + "maker_blueprint_id": "00000000-0000-4000-8000-000000000004", + "staged_at": "2026-01-01T00:00:00Z", + }, + "metadata": {}, + "blueprint": { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000005", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000005", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + "notes": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": ["user@example.com"], + "is_internal": True, + "attachments": [ + { + "s3_key": "sample", + "filename": "sample", + "file_size": 1, + "url": "https://example.com/resource", + } + ], + "tags": ["sample"], + "created_at": "2026-01-01T00:00:00Z", + } + ], + "events": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "event_type": "CREATED", + "actor_email": "user@example.com", + "actor_name": "sample", + "previous_status": "OPEN", + "new_status": "OPEN", + "metadata": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000006", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + "links_count": 1, + "notes_count": 1, + "checklist_items": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseDetail, + ), + lambda c: c.cases.update("c1a5e000-0000-4000-8000-000000000001", title="sample"), + lambda c: c.cases.update("c1a5e000-0000-4000-8000-000000000001", title="sample"), + lambda r: isinstance(r, CaseDetail), + lambda r: isinstance(r, CaseDetail), + ) + + +def test_cases_blueprints_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/", + api="verification", + auth="api_key", + json_body={"name": "sample", "assignee_pool": ["00000000-0000-4000-8000-000000000002"]}, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000004"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000003", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000003", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": { + "enabled": True, + "optional": True, + "items": [{"label": "sample", "order": 1}], + }, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": {"enabled": True, "payment_methods": True, "transactions": True}, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + strict_type=CaseBlueprintDetail, + ), + lambda c: c.cases.blueprints.create( + name="sample", assignee_pool=["00000000-0000-4000-8000-000000000002"] + ), + lambda c: c.cases.blueprints.create( + name="sample", assignee_pool=["00000000-0000-4000-8000-000000000002"] + ), + lambda r: isinstance(r, CaseBlueprintDetail), + lambda r: isinstance(r, CaseBlueprintDetail), + ) + + +def test_cases_blueprints_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/b1de0000-0000-4000-8000-000000000002/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.cases.blueprints.delete("b1de0000-0000-4000-8000-000000000002"), + lambda c: c.cases.blueprints.delete("b1de0000-0000-4000-8000-000000000002"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_cases_blueprints_discard_draft() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/00000000-0000-4000-8000-000000000001/draft/", + api="verification", + auth="api_key", + json_body={ + "expected_version": 1, + "expected_revision": "00000000-0000-4000-8000-000000000001", + }, + status=204, + ), + lambda c: c.cases.blueprints.discard_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.blueprints.discard_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_cases_blueprints_install_presets() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/install/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/presets/install/", + api="verification", + auth="api_key", + json_body={"preset_keys": ["sample"]}, + status=200, + response_json={"created": ["sample"]}, + strict_type=CasesBlueprintsInstallPresetsResponse, + ), + lambda c: c.cases.blueprints.install_presets(preset_keys=["sample"]), + lambda c: c.cases.blueprints.install_presets(preset_keys=["sample"]), + lambda r: isinstance(r, CasesBlueprintsInstallPresetsResponse), + lambda r: isinstance(r, CasesBlueprintsInstallPresetsResponse), + ) + + +def test_cases_blueprints_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000006"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + } + ], + }, + ), + lambda c: c.cases.blueprints.list(limit=1, offset=1), + lambda c: c.cases.blueprints.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, CaseBlueprintListItem) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, CaseBlueprintListItem) for x1 in r.results) + ), + ) + + +def test_cases_blueprints_list_history() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/history/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/00000000-0000-4000-8000-000000000001/history/", + api="verification", + auth="api_key", + query={"ordering": ["sample"], "limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 123, + "next": "http://api.example.org/accounts/?offset=400&limit=100", + "previous": "http://api.example.org/accounts/?offset=200&limit=100", + "results": [ + { + "version": 1, + "snapshot": "value", + "actor_email": "user@example.com", + "published_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.cases.blueprints.list_history( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda c: c.cases.blueprints.list_history( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, CasePublicationPolicyRevision) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, CasePublicationPolicyRevision) for x1 in r.results) + ), + ) + + +def test_cases_blueprints_list_presets() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/presets/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/presets/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "preset_key": "screening_hit", + "name": "sample", + "description": "sample", + "content_config": { + "checklist": {"enabled": True, "optional": True, "items": [{}]}, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": { + "enabled": True, + "payment_methods": True, + "transactions": True, + }, + }, + "installed": True, + } + ] + }, + strict_type=CasesBlueprintsListPresetsResponse, + ), + lambda c: c.cases.blueprints.list_presets(), + lambda c: c.cases.blueprints.list_presets(), + lambda r: isinstance(r, CasesBlueprintsListPresetsResponse), + lambda r: isinstance(r, CasesBlueprintsListPresetsResponse), + ) + + +def test_cases_blueprints_publish() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/publish/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/00000000-0000-4000-8000-000000000001/publish/", + api="verification", + auth="api_key", + json_body={ + "expected_version": 1, + "expected_revision": "00000000-0000-4000-8000-000000000001", + }, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": "value", + "max_active_cases": 1, + "escalation_enabled": True, + "escalation_blueprint": "00000000-0000-4000-8000-000000000003", + "escalation_blueprint_name": "sample", + "transfer_enabled": True, + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000003", + "four_eyes_checker_blueprint_name": "sample", + "require_resolution_note": True, + "content_config": "value", + "version": 1, + "default_deadline_hours": 1, + "is_active": True, + "preset_key": "sample", + "creation_sources": [{}], + "receives_escalation_from": [{}], + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CasePublicationCaseBlueprint, + ), + lambda c: c.cases.blueprints.publish( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.blueprints.publish( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, CasePublicationCaseBlueprint), + lambda r: isinstance(r, CasePublicationCaseBlueprint), + ) + + +def test_cases_blueprints_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/b1de0000-0000-4000-8000-000000000002/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000004"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000003", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000003", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": { + "enabled": True, + "optional": True, + "items": [{"label": "sample", "order": 1}], + }, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": {"enabled": True, "payment_methods": True, "transactions": True}, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + strict_type=CaseBlueprintDetail, + ), + lambda c: c.cases.blueprints.retrieve("b1de0000-0000-4000-8000-000000000002"), + lambda c: c.cases.blueprints.retrieve("b1de0000-0000-4000-8000-000000000002"), + lambda r: isinstance(r, CaseBlueprintDetail), + lambda r: isinstance(r, CaseBlueprintDetail), + ) + + +def test_cases_blueprints_retrieve_draft() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/00000000-0000-4000-8000-000000000001/draft/", + api="verification", + auth="api_key", + status=200, + response_json={ + "draft": { + "revision": "00000000-0000-4000-8000-000000000005", + "base_version": 1, + "changes": "value", + "actor_email": "user@example.com", + "updated_at": "2026-01-01T00:00:00Z", + } + }, + strict_type=CasePublicationPolicyDraftResponse, + ), + lambda c: c.cases.blueprints.retrieve_draft("00000000-0000-4000-8000-000000000001"), + lambda c: c.cases.blueprints.retrieve_draft("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CasePublicationPolicyDraftResponse), + lambda r: isinstance(r, CasePublicationPolicyDraftResponse), + ) + + +def test_cases_blueprints_save_draft() -> None: + """``PUT /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/draft/``""" + check_operation( + Expect( + method="PUT", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/00000000-0000-4000-8000-000000000001/draft/", + api="verification", + auth="api_key", + json_body={ + "expected_version": 1, + "expected_revision": "00000000-0000-4000-8000-000000000001", + "changes": {}, + }, + status=200, + response_json={ + "revision": "00000000-0000-4000-8000-000000000003", + "base_version": 1, + "changes": "value", + "actor_email": "user@example.com", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CasePublicationPolicyDraft, + ), + lambda c: c.cases.blueprints.save_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + changes={}, + ), + lambda c: c.cases.blueprints.save_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + changes={}, + ), + lambda r: isinstance(r, CasePublicationPolicyDraft), + lambda r: isinstance(r, CasePublicationPolicyDraft), + ) + + +def test_cases_blueprints_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/blueprints/{blueprint_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/blueprints/b1de0000-0000-4000-8000-000000000002/", + api="verification", + auth="api_key", + json_body={"name": "sample"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "description": "sample", + "assignment_method": "MANUAL", + "assignee_pool": ["00000000-0000-4000-8000-000000000004"], + "max_active_cases": 1, + "escalation_enabled": True, + "transfer_enabled": True, + "require_resolution_note": True, + "is_active": True, + "preset_key": "sample", + "created_at": "2026-01-01T00:00:00Z", + "escalation_blueprint": "00000000-0000-4000-8000-000000000003", + "escalation_blueprint_name": "sample", + "four_eyes_checker_blueprint": "00000000-0000-4000-8000-000000000003", + "four_eyes_checker_blueprint_name": "sample", + "content_config": { + "checklist": { + "enabled": True, + "optional": True, + "items": [{"label": "sample", "order": 1}], + }, + "aml_control": {"enabled": True, "financial": True, "identity": True}, + "identity": { + "enabled": True, + "personal_information": True, + "applicant_tags": True, + "contact_information": True, + "risk_overview": True, + "poi_documents": True, + "poa_documents": True, + }, + "verifications": {"enabled": True, "summary": True}, + "reporting": {"enabled": True, "fiu_reports": True}, + "financial": {"enabled": True, "payment_methods": True, "transactions": True}, + }, + "creation_sources": [{"kind": "aml", "id": "sample", "name": "sample"}], + "receives_escalation_from": [{"id": "sample", "name": "sample"}], + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "default_deadline_hours": 1, + }, + strict_type=CaseBlueprintDetail, + ), + lambda c: c.cases.blueprints.update("b1de0000-0000-4000-8000-000000000002", name="sample"), + lambda c: c.cases.blueprints.update("b1de0000-0000-4000-8000-000000000002", name="sample"), + lambda r: isinstance(r, CaseBlueprintDetail), + lambda r: isinstance(r, CaseBlueprintDetail), + ) + + +def test_cases_checklist_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/checklist/", + api="verification", + auth="api_key", + status=200, + response_json=[ + { + "uuid": "00000000-0000-4000-8000-000000000004", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + } + ], + strict_type=list[CaseChecklistItem], + ), + lambda c: c.cases.checklist.list("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.checklist.list("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, list) and all(isinstance(x1, CaseChecklistItem) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, CaseChecklistItem) for x1 in r), + ) + + +def test_cases_checklist_toggle_item() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/checklist/{item_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/checklist/c4ec0000-0000-4000-8000-000000000005/", + api="verification", + auth="api_key", + json_body={"is_completed": True}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "label": "sample", + "is_completed": True, + "completed_by": "user@example.com", + "completed_at": "2026-01-01T00:00:00Z", + "order": 1, + }, + strict_type=CaseChecklistItem, + ), + lambda c: c.cases.checklist.toggle_item( + "c1a5e000-0000-4000-8000-000000000001", + "c4ec0000-0000-4000-8000-000000000005", + is_completed=True, + ), + lambda c: c.cases.checklist.toggle_item( + "c1a5e000-0000-4000-8000-000000000001", + "c4ec0000-0000-4000-8000-000000000005", + is_completed=True, + ), + lambda r: isinstance(r, CaseChecklistItem), + lambda r: isinstance(r, CaseChecklistItem), + ) + + +def test_cases_links_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/links/", + api="verification", + auth="api_key", + json_body={ + "entity_type": "session", + "entity_uuid": "00000000-0000-4000-8000-000000000001", + }, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000003", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseLink, + ), + lambda c: c.cases.links.create( + "c1a5e000-0000-4000-8000-000000000001", + entity_type="session", + entity_uuid="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.links.create( + "c1a5e000-0000-4000-8000-000000000001", + entity_type="session", + entity_uuid="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, CaseLink), + lambda r: isinstance(r, CaseLink), + ) + + +def test_cases_links_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/{link_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/links/11000000-0000-4000-8000-000000000004/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.cases.links.delete( + "c1a5e000-0000-4000-8000-000000000001", "11000000-0000-4000-8000-000000000004" + ), + lambda c: c.cases.links.delete( + "c1a5e000-0000-4000-8000-000000000001", "11000000-0000-4000-8000-000000000004" + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_cases_links_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/links/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/links/", + api="verification", + auth="api_key", + status=200, + response_json=[ + { + "uuid": "00000000-0000-4000-8000-000000000004", + "linked_entity_type": "session", + "linked_entity_uuid": "00000000-0000-4000-8000-000000000004", + "linked_entity_summary": {}, + "created_at": "2026-01-01T00:00:00Z", + } + ], + strict_type=list[CaseLink], + ), + lambda c: c.cases.links.list("c1a5e000-0000-4000-8000-000000000001"), + lambda c: c.cases.links.list("c1a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, list) and all(isinstance(x1, CaseLink) for x1 in r), + lambda r: isinstance(r, list) and all(isinstance(x1, CaseLink) for x1 in r), + ) + + +def test_cases_notes_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/notes/", + api="verification", + auth="api_key", + json_body={"comment": "sample"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": "value", + "is_internal": True, + "attachments": "value", + "tags": "value", + "created_at": "2026-01-01T00:00:00Z", + "reply_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "actor_name": "sample", + "actor_email": "user@example.com", + "comment": "sample", + }, + "is_pinned": True, + "client_reference": "00000000-0000-4000-8000-000000000003", + }, + strict_type=CaseCollaborationCaseNote, + ), + lambda c: c.cases.notes.create("00000000-0000-4000-8000-000000000001", comment="sample"), + lambda c: c.cases.notes.create("00000000-0000-4000-8000-000000000001", comment="sample"), + lambda r: isinstance(r, CaseCollaborationCaseNote), + lambda r: isinstance(r, CaseCollaborationCaseNote), + ) + + +def test_cases_notes_create_attachment_upload() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/notes/presign/", + api="verification", + auth="api_key", + multipart_fields=[], + multipart_files=["file"], + status=201, + response_json={ + "s3_key": "sample", + "url": "https://example.com/resource", + "filename": "sample", + "file_size": 1, + }, + strict_type=CasesNotesCreateAttachmentUploadResponse, + ), + lambda c: c.cases.notes.create_attachment_upload( + "c1a5e000-0000-4000-8000-000000000001", file=b"file-content" + ), + lambda c: c.cases.notes.create_attachment_upload( + "c1a5e000-0000-4000-8000-000000000001", file=b"file-content" + ), + lambda r: isinstance(r, CasesNotesCreateAttachmentUploadResponse), + lambda r: isinstance(r, CasesNotesCreateAttachmentUploadResponse), + ) + + +def test_cases_notes_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/notes/00000000-0000-4000-8000-000000000002/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.cases.notes.delete( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.cases.notes.delete( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_cases_notes_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/notes/", + api="verification", + auth="api_key", + query={"ordering": ["sample"], "limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 123, + "next": "http://api.example.org/accounts/?offset=400&limit=100", + "previous": "http://api.example.org/accounts/?offset=200&limit=100", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": "value", + "is_internal": True, + "attachments": "value", + "tags": "value", + "created_at": "2026-01-01T00:00:00Z", + "reply_to": { + "uuid": "00000000-0000-4000-8000-000000000008", + "actor_name": "sample", + "actor_email": "user@example.com", + "comment": "sample", + }, + "is_pinned": True, + "client_reference": "00000000-0000-4000-8000-000000000006", + } + ], + }, + ), + lambda c: c.cases.notes.list( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda c: c.cases.notes.list( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, CaseCollaborationCaseNote) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, CaseCollaborationCaseNote) for x1 in r.results) + ), + ) + + +def test_cases_notes_retrieve_attachment_url() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/presign/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/notes/presign/", + api="verification", + auth="api_key", + query={"s3_key": ["sample"]}, + status=200, + response_json={ + "s3_key": "sample", + "url": "https://example.com/resource", + "filename": "sample", + }, + strict_type=CasesNotesRetrieveAttachmentUrlResponse, + ), + lambda c: c.cases.notes.retrieve_attachment_url( + "c1a5e000-0000-4000-8000-000000000001", s3_key="sample" + ), + lambda c: c.cases.notes.retrieve_attachment_url( + "c1a5e000-0000-4000-8000-000000000001", s3_key="sample" + ), + lambda r: isinstance(r, CasesNotesRetrieveAttachmentUrlResponse), + lambda r: isinstance(r, CasesNotesRetrieveAttachmentUrlResponse), + ) + + +def test_cases_notes_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/notes/{note_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/notes/00000000-0000-4000-8000-000000000002/", + api="verification", + auth="api_key", + json_body={"is_pinned": True}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "comment": "sample", + "actor_email": "user@example.com", + "actor_name": "sample", + "mentioned_emails": "value", + "is_internal": True, + "attachments": "value", + "tags": "value", + "created_at": "2026-01-01T00:00:00Z", + "reply_to": { + "uuid": "00000000-0000-4000-8000-000000000005", + "actor_name": "sample", + "actor_email": "user@example.com", + "comment": "sample", + }, + "is_pinned": True, + "client_reference": "00000000-0000-4000-8000-000000000003", + }, + strict_type=CaseCollaborationCaseNote, + ), + lambda c: c.cases.notes.update( + "00000000-0000-4000-8000-000000000001", + "00000000-0000-4000-8000-000000000002", + is_pinned=True, + ), + lambda c: c.cases.notes.update( + "00000000-0000-4000-8000-000000000001", + "00000000-0000-4000-8000-000000000002", + is_pinned=True, + ), + lambda r: isinstance(r, CaseCollaborationCaseNote), + lambda r: isinstance(r, CaseCollaborationCaseNote), + ) + + +def test_cases_regulatory_reports_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/regulatory-reports/", + api="verification", + auth="api_key", + json_body={"report_type": "SAR"}, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000004"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RegulatoryReportDetail, + ), + lambda c: c.cases.regulatory_reports.create( + "c1a5e000-0000-4000-8000-000000000001", report_type="SAR" + ), + lambda c: c.cases.regulatory_reports.create( + "c1a5e000-0000-4000-8000-000000000001", report_type="SAR" + ), + lambda r: isinstance(r, RegulatoryReportDetail), + lambda r: isinstance(r, RegulatoryReportDetail), + ) + + +def test_cases_regulatory_reports_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/regulatory-reports/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/c1a5e000-0000-4000-8000-000000000001/regulatory-reports/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "case_uuid": "00000000-0000-4000-8000-000000000005", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000006"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.cases.regulatory_reports.list( + "c1a5e000-0000-4000-8000-000000000001", limit=1, offset=1 + ), + lambda c: c.cases.regulatory_reports.list( + "c1a5e000-0000-4000-8000-000000000001", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, RegulatoryReportDetail) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, RegulatoryReportDetail) for x1 in r.results) + ), + ) + + +def test_cases_requests_cancel() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/cancel/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/00000000-0000-4000-8000-000000000002/cancel/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "purpose": "sample", + "language": "sample", + "recipient": "user@example.com", + "expires_at": "2026-01-01T00:00:00Z", + "actor_email": "user@example.com", + "delivery_status": "sample", + "delivered_at": "2026-01-01T00:00:00Z", + "cancelled_at": "2026-01-01T00:00:00Z", + "responded_at": "2026-01-01T00:00:00Z", + "response": "value", + "has_attachment": True, + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=CustomerMessagingCaseRequest, + ), + lambda c: c.cases.requests.cancel( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.cases.requests.cancel( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: isinstance(r, CustomerMessagingCaseRequest), + lambda r: isinstance(r, CustomerMessagingCaseRequest), + ) + + +def test_cases_requests_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/", + api="verification", + auth="api_key", + json_body={ + "contact_id": "00000000-0000-4000-8000-000000000001", + "purpose": "identity_document", + "request_id": "00000000-0000-4000-8000-000000000001", + "expires_at": "2026-01-01T00:00:00Z", + "preview_token": "sample-token", + }, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "purpose": "sample", + "language": "sample", + "recipient": "user@example.com", + "expires_at": "2026-01-01T00:00:00Z", + "actor_email": "user@example.com", + "delivery_status": "sample", + "delivered_at": "2026-01-01T00:00:00Z", + "cancelled_at": "2026-01-01T00:00:00Z", + "responded_at": "2026-01-01T00:00:00Z", + "response": "value", + "has_attachment": True, + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=CustomerMessagingCaseRequest, + ), + lambda c: c.cases.requests.create( + "00000000-0000-4000-8000-000000000001", + contact_id="00000000-0000-4000-8000-000000000001", + purpose="identity_document", + request_id="00000000-0000-4000-8000-000000000001", + expires_at="2026-01-01T00:00:00Z", + preview_token="sample-token", + ), + lambda c: c.cases.requests.create( + "00000000-0000-4000-8000-000000000001", + contact_id="00000000-0000-4000-8000-000000000001", + purpose="identity_document", + request_id="00000000-0000-4000-8000-000000000001", + expires_at="2026-01-01T00:00:00Z", + preview_token="sample-token", + ), + lambda r: isinstance(r, CustomerMessagingCaseRequest), + lambda r: isinstance(r, CustomerMessagingCaseRequest), + ) + + +def test_cases_requests_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/", + api="verification", + auth="api_key", + query={"ordering": ["sample"], "limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 123, + "next": "http://api.example.org/accounts/?offset=400&limit=100", + "previous": "http://api.example.org/accounts/?offset=200&limit=100", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "purpose": "sample", + "language": "sample", + "recipient": "user@example.com", + "expires_at": "2026-01-01T00:00:00Z", + "actor_email": "user@example.com", + "delivery_status": "sample", + "delivered_at": "2026-01-01T00:00:00Z", + "cancelled_at": "2026-01-01T00:00:00Z", + "responded_at": "2026-01-01T00:00:00Z", + "response": "value", + "has_attachment": True, + "created_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.cases.requests.list( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda c: c.cases.requests.list( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, CustomerMessagingCaseRequest) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, CustomerMessagingCaseRequest) for x1 in r.results) + ), + ) + + +def test_cases_requests_preview() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/preview/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/preview/", + api="verification", + auth="api_key", + json_body={ + "contact_id": "00000000-0000-4000-8000-000000000001", + "purpose": "identity_document", + }, + status=200, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000003", + "expires_at": "2026-01-01T00:00:00Z", + "recipient": "user@example.com", + "sender": "sample", + "subject": "sample", + "html": "sample", + "requires_user_action": True, + "preview_token": "sample-token", + }, + strict_type=CustomerMessagingCaseRequestPreviewResponse, + ), + lambda c: c.cases.requests.preview( + "00000000-0000-4000-8000-000000000001", + contact_id="00000000-0000-4000-8000-000000000001", + purpose="identity_document", + ), + lambda c: c.cases.requests.preview( + "00000000-0000-4000-8000-000000000001", + contact_id="00000000-0000-4000-8000-000000000001", + purpose="identity_document", + ), + lambda r: isinstance(r, CustomerMessagingCaseRequestPreviewResponse), + lambda r: isinstance(r, CustomerMessagingCaseRequestPreviewResponse), + ) + + +def test_cases_requests_retrieve_attachment() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/{request_uuid}/attachment/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/00000000-0000-4000-8000-000000000002/attachment/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="image/jpeg", + ), + lambda c: c.cases.requests.retrieve_attachment( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.cases.requests.retrieve_attachment( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_cases_requests_retrieve_options() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/{case_uuid}/requests/options/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/00000000-0000-4000-8000-000000000001/requests/options/", + api="verification", + auth="api_key", + status=200, + response_json={ + "contacts": [ + { + "uuid": "00000000-0000-4000-8000-000000000006", + "email": "user@example.com", + "verified_at": "2026-01-01T00:00:00Z", + } + ], + "purposes": ["identity_document"], + "languages": ["en"], + }, + strict_type=CustomerMessagingCaseRequestOptions, + ), + lambda c: c.cases.requests.retrieve_options("00000000-0000-4000-8000-000000000001"), + lambda c: c.cases.requests.retrieve_options("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CustomerMessagingCaseRequestOptions), + lambda r: isinstance(r, CustomerMessagingCaseRequestOptions), + ) + + +def test_cases_routing_rules_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/", + api="verification", + auth="api_key", + json_body={ + "name": "sample", + "conditions": [{"field": "source", "operator": "equals", "value": "sample"}], + "blueprint": "00000000-0000-4000-8000-000000000001", + }, + status=201, + response_json={ + "name": "sample", + "description": "sample", + "priority": 1, + "conditions": [{"field": "source", "operator": "equals", "value": "sample"}], + "required_skills": ["sample"], + "blueprint": "00000000-0000-4000-8000-000000000003", + "team": "00000000-0000-4000-8000-000000000003", + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "blueprint_name": "sample", + "team_name": "sample", + }, + strict_type=CaseRoutingRule, + ), + lambda c: c.cases.routing_rules.create( + name="sample", + conditions=[{"field": "source", "operator": "equals", "value": "sample"}], + blueprint="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.routing_rules.create( + name="sample", + conditions=[{"field": "source", "operator": "equals", "value": "sample"}], + blueprint="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, CaseRoutingRule), + lambda r: isinstance(r, CaseRoutingRule), + ) + + +def test_cases_routing_rules_discard_draft() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/draft/", + api="verification", + auth="api_key", + json_body={ + "expected_version": 1, + "expected_revision": "00000000-0000-4000-8000-000000000001", + }, + status=204, + ), + lambda c: c.cases.routing_rules.discard_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.routing_rules.discard_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_cases_routing_rules_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "results": [ + { + "name": "sample", + "description": "sample", + "priority": 1, + "conditions": [ + {"field": "source", "operator": "equals", "value": "sample"} + ], + "required_skills": ["sample"], + "blueprint": "00000000-0000-4000-8000-000000000005", + "team": "00000000-0000-4000-8000-000000000005", + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000005", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "blueprint_name": "sample", + "team_name": "sample", + } + ], + "count": 1, + "next": "sample", + "previous": "sample", + }, + ), + lambda c: c.cases.routing_rules.list(limit=1, offset=1), + lambda c: c.cases.routing_rules.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, CaseRoutingRule) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, CaseRoutingRule) for x1 in r.results) + ), + ) + + +def test_cases_routing_rules_list_history() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/history/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/history/", + api="verification", + auth="api_key", + query={"ordering": ["sample"], "limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 123, + "next": "http://api.example.org/accounts/?offset=400&limit=100", + "previous": "http://api.example.org/accounts/?offset=200&limit=100", + "results": [ + { + "version": 1, + "snapshot": "value", + "actor_email": "user@example.com", + "published_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.cases.routing_rules.list_history( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda c: c.cases.routing_rules.list_history( + "00000000-0000-4000-8000-000000000001", ordering="sample", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, RoutingPublicationPolicyRevision) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, RoutingPublicationPolicyRevision) for x1 in r.results) + ), + ) + + +def test_cases_routing_rules_preview() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/preview/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/preview/", + api="verification", + auth="api_key", + json_body={"source": "MANUAL"}, + status=200, + response_json={ + "rule_uuid": "00000000-0000-4000-8000-000000000003", + "rule_name": "sample", + "rule_version": 1, + "blueprint_uuid": "00000000-0000-4000-8000-000000000003", + "team_uuid": "00000000-0000-4000-8000-000000000003", + "assignee_user_id": "00000000-0000-4000-8000-000000000003", + "reason": "sample", + "candidates": [ + { + "user_id": "00000000-0000-4000-8000-000000000005", + "eligible": True, + "active_cases": 1, + "reason": "sample", + } + ], + "matched_rules": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "priority": 1, + } + ], + }, + strict_type=CaseRoutingPreview, + ), + lambda c: c.cases.routing_rules.preview(source="MANUAL"), + lambda c: c.cases.routing_rules.preview(source="MANUAL"), + lambda r: isinstance(r, CaseRoutingPreview), + lambda r: isinstance(r, CaseRoutingPreview), + ) + + +def test_cases_routing_rules_publish() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/publish/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/publish/", + api="verification", + auth="api_key", + json_body={ + "expected_version": 1, + "expected_revision": "00000000-0000-4000-8000-000000000001", + }, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "description": "sample", + "priority": 1, + "conditions": [{"field": "source", "operator": "equals", "value": "value"}], + "required_skills": ["sample"], + "blueprint": "00000000-0000-4000-8000-000000000003", + "blueprint_name": "sample", + "team": "00000000-0000-4000-8000-000000000003", + "team_name": "sample", + "is_active": True, + "version": 1, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RoutingPublicationCaseRoutingRule, + ), + lambda c: c.cases.routing_rules.publish( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.cases.routing_rules.publish( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, RoutingPublicationCaseRoutingRule), + lambda r: isinstance(r, RoutingPublicationCaseRoutingRule), + ) + + +def test_cases_routing_rules_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "name": "sample", + "description": "sample", + "priority": 1, + "conditions": [{"field": "source", "operator": "equals", "value": "sample"}], + "required_skills": ["sample"], + "blueprint": "00000000-0000-4000-8000-000000000003", + "team": "00000000-0000-4000-8000-000000000003", + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "blueprint_name": "sample", + "team_name": "sample", + }, + strict_type=CaseRoutingRule, + ), + lambda c: c.cases.routing_rules.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.cases.routing_rules.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseRoutingRule), + lambda r: isinstance(r, CaseRoutingRule), + ) + + +def test_cases_routing_rules_retrieve_draft() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/draft/", + api="verification", + auth="api_key", + status=200, + response_json={ + "draft": { + "revision": "00000000-0000-4000-8000-000000000005", + "base_version": 1, + "changes": "value", + "actor_email": "user@example.com", + "updated_at": "2026-01-01T00:00:00Z", + } + }, + strict_type=RoutingPublicationPolicyDraftResponse, + ), + lambda c: c.cases.routing_rules.retrieve_draft("00000000-0000-4000-8000-000000000001"), + lambda c: c.cases.routing_rules.retrieve_draft("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, RoutingPublicationPolicyDraftResponse), + lambda r: isinstance(r, RoutingPublicationPolicyDraftResponse), + ) + + +def test_cases_routing_rules_save_draft() -> None: + """``PUT /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/draft/``""" + check_operation( + Expect( + method="PUT", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/draft/", + api="verification", + auth="api_key", + json_body={ + "expected_version": 1, + "expected_revision": "00000000-0000-4000-8000-000000000001", + "changes": {}, + }, + status=200, + response_json={ + "revision": "00000000-0000-4000-8000-000000000003", + "base_version": 1, + "changes": "value", + "actor_email": "user@example.com", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RoutingPublicationPolicyDraft, + ), + lambda c: c.cases.routing_rules.save_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + changes={}, + ), + lambda c: c.cases.routing_rules.save_draft( + "00000000-0000-4000-8000-000000000001", + expected_version=1, + expected_revision="00000000-0000-4000-8000-000000000001", + changes={}, + ), + lambda r: isinstance(r, RoutingPublicationPolicyDraft), + lambda r: isinstance(r, RoutingPublicationPolicyDraft), + ) + + +def test_cases_routing_rules_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/routing-rules/{record_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/routing-rules/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"name": "sample"}, + status=200, + response_json={ + "name": "sample", + "description": "sample", + "priority": 1, + "conditions": [{"field": "source", "operator": "equals", "value": "sample"}], + "required_skills": ["sample"], + "blueprint": "00000000-0000-4000-8000-000000000003", + "team": "00000000-0000-4000-8000-000000000003", + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "version": 1, + "blueprint_name": "sample", + "team_name": "sample", + }, + strict_type=CaseRoutingRule, + ), + lambda c: c.cases.routing_rules.update( + "00000000-0000-4000-8000-000000000001", name="sample" + ), + lambda c: c.cases.routing_rules.update( + "00000000-0000-4000-8000-000000000001", name="sample" + ), + lambda r: isinstance(r, CaseRoutingRule), + lambda r: isinstance(r, CaseRoutingRule), + ) + + +def test_cases_teams_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/cases/teams/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/teams/", + api="verification", + auth="api_key", + json_body={ + "name": "sample", + "members": [{"user_id": "00000000-0000-4000-8000-000000000004"}], + }, + status=201, + response_json={ + "name": "sample", + "description": "sample", + "members": [ + { + "user_id": "00000000-0000-4000-8000-000000000006", + "availability": "available", + "capacity": 1, + "skills": ["sample"], + "available_from": "2026-01-01T00:00:00Z", + "available_until": "2026-01-01T00:00:00Z", + } + ], + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseReviewTeam, + ), + lambda c: c.cases.teams.create( + name="sample", members=[{"user_id": "00000000-0000-4000-8000-000000000004"}] + ), + lambda c: c.cases.teams.create( + name="sample", members=[{"user_id": "00000000-0000-4000-8000-000000000004"}] + ), + lambda r: isinstance(r, CaseReviewTeam), + lambda r: isinstance(r, CaseReviewTeam), + ) + + +def test_cases_teams_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/teams/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "results": [ + { + "name": "sample", + "description": "sample", + "members": [ + { + "user_id": "00000000-0000-4000-8000-000000000008", + "availability": "available", + "capacity": 1, + "skills": [], + "available_from": "2026-01-01T00:00:00Z", + "available_until": "2026-01-01T00:00:00Z", + } + ], + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000005", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ], + "count": 1, + "next": "sample", + "previous": "sample", + }, + ), + lambda c: c.cases.teams.list(limit=1, offset=1), + lambda c: c.cases.teams.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, CaseReviewTeam) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, CaseReviewTeam) for x1 in r.results) + ), + ) + + +def test_cases_teams_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/teams/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "name": "sample", + "description": "sample", + "members": [ + { + "user_id": "00000000-0000-4000-8000-000000000006", + "availability": "available", + "capacity": 1, + "skills": ["sample"], + "available_from": "2026-01-01T00:00:00Z", + "available_until": "2026-01-01T00:00:00Z", + } + ], + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseReviewTeam, + ), + lambda c: c.cases.teams.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.cases.teams.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, CaseReviewTeam), + lambda r: isinstance(r, CaseReviewTeam), + ) + + +def test_cases_teams_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/cases/teams/{record_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/cases/teams/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"name": "sample"}, + status=200, + response_json={ + "name": "sample", + "description": "sample", + "members": [ + { + "user_id": "00000000-0000-4000-8000-000000000006", + "availability": "available", + "capacity": 1, + "skills": ["sample"], + "available_from": "2026-01-01T00:00:00Z", + "available_until": "2026-01-01T00:00:00Z", + } + ], + "is_active": True, + "uuid": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=CaseReviewTeam, + ), + lambda c: c.cases.teams.update("00000000-0000-4000-8000-000000000001", name="sample"), + lambda c: c.cases.teams.update("00000000-0000-4000-8000-000000000001", name="sample"), + lambda r: isinstance(r, CaseReviewTeam), + lambda r: isinstance(r, CaseReviewTeam), + ) diff --git a/tests/generated/test_customization.py b/tests/generated/test_customization.py new file mode 100644 index 0000000..a4ec419 --- /dev/null +++ b/tests/generated/test_customization.py @@ -0,0 +1,115 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``customization`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.branding_customization import BrandingCustomization +from tests.harness import Expect, check_operation + + +def test_customization_retrieve() -> None: + """``GET /v3/customization/``""" + check_operation( + Expect( + method="GET", + path="/v3/customization/", + api="verification", + auth="api_key", + status=200, + response_json={ + "color_primary": "#000000", + "color_secondary": "#9DA1A1", + "color_background": "#FFFFFF", + "color_panel": "#2567FF", + "color_panel_10": "#1A1A1A", + "color_on_panel_1": "#9DA1A1", + "color_on_panel_2": "#F4F4F6", + "color_on_background": "#000000", + "color_button_1": "#2567FF", + "color_button_2": "#F8F8F8", + "color_button_text_1": "#FFFFFF", + "color_button_text_2": "#000000", + "color_pill_text": "#111827", + "border_radius_panel": 10, + "border_radius_buttons": 12, + "font_family": "Inter", + "font_weight": "regular", + "font_url": "https://fonts.googleapis.com/css2?family=Inter", + "logo_square": None, + "logo_rectangular": None, + "favicon": None, + "app_public_name": "Acme Verify", + "privacy_policy_url": "https://acme.com/privacy", + "skip_welcome_screen": False, + "disable_login_with_didit": False, + "hide_progress_bar": False, + "callback_seconds": 3, + }, + ), + lambda c: c.customization.retrieve(), + lambda c: c.customization.retrieve(), + lambda r: isinstance(r, BrandingCustomization), + lambda r: isinstance(r, BrandingCustomization), + ) + + +def test_customization_update() -> None: + """``PATCH /v3/customization/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/customization/", + api="verification", + auth="api_key", + json_body={ + "color_primary": "#123456", + "color_button_1": "#2567FF", + "border_radius_buttons": 16, + "font_family": "AR One Sans", + }, + status=200, + response_json={ + "color_primary": "#123456", + "color_secondary": "#9DA1A1", + "color_background": "#FFFFFF", + "color_panel": "#2567FF", + "color_panel_10": "#1A1A1A", + "color_on_panel_1": "#9DA1A1", + "color_on_panel_2": "#F4F4F6", + "color_on_background": "#000000", + "color_button_1": "#2567FF", + "color_button_2": "#F8F8F8", + "color_button_text_1": "#FFFFFF", + "color_button_text_2": "#000000", + "color_pill_text": "#111827", + "border_radius_panel": 10, + "border_radius_buttons": 16, + "font_family": "AR One Sans", + "font_weight": "400", + "font_url": "https://fonts.googleapis.com/css2?family=AR+One+Sans", + "logo_square": None, + "logo_rectangular": "https://cdn.didit.me/white_label/logo_rectangular/...", + "favicon": None, + "app_public_name": "Acme Verify", + "privacy_policy_url": "https://acme.com/privacy", + "skip_welcome_screen": True, + "disable_login_with_didit": False, + "hide_progress_bar": False, + "callback_seconds": 3, + }, + ), + lambda c: c.customization.update( + color_primary="#123456", + color_button_1="#2567FF", + border_radius_buttons=16, + font_family="AR One Sans", + ), + lambda c: c.customization.update( + color_primary="#123456", + color_button_1="#2567FF", + border_radius_buttons=16, + font_family="AR One Sans", + ), + lambda r: isinstance(r, BrandingCustomization), + lambda r: isinstance(r, BrandingCustomization), + ) diff --git a/tests/generated/test_database_validation.py b/tests/generated/test_database_validation.py new file mode 100644 index 0000000..3ab5a3e --- /dev/null +++ b/tests/generated/test_database_validation.py @@ -0,0 +1,66 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``database_validation`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.database_validation.validate_response import DatabaseValidationValidateResponse +from tests.harness import Expect, check_operation + + +def test_database_validation_validate() -> None: + """``POST /v3/database-validation/``""" + check_operation( + Expect( + method="POST", + path="/v3/database-validation/", + api="verification", + auth="api_key", + json_body={"issuing_state": "BRA"}, + status=200, + response_json={ + "request_id": "7f9a1c0e-1f2b-4f6e-9d3a-2b1c0e7f9a1c", + "database_validation": { + "status": "Approved", + "issuing_state": "BRA", + "validation_type": "one_by_one", + "screened_data": { + "tax_number": "12345678900", + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1980-01-01", + }, + "match_type": "full_match", + "validations": [ + { + "validation": { + "full_name": "full_match", + "date_of_birth": "full_match", + "identification_number": "full_match", + }, + "outcome_code": "MATCH", + "outcome_detail": "200", + "service_id": "bra_cpf", + "service_name": "Brazil - CPF status check", + "source_data": { + "identification_number": "12345678900", + "first_name": "JOHN", + "last_name": "DOE", + "date_of_birth": "1980-01-01", + "lgpd_minor": False, + "minor_under_16": False, + "minor_under_18": False, + }, + } + ], + "warnings": [], + }, + "vendor_data": "user-1234", + "metadata": None, + "created_at": "2026-06-11T10:30:00.000000+00:00", + }, + ), + lambda c: c.database_validation.validate(issuing_state="BRA"), + lambda c: c.database_validation.validate(issuing_state="BRA"), + lambda r: isinstance(r, DatabaseValidationValidateResponse), + lambda r: isinstance(r, DatabaseValidationValidateResponse), + ) diff --git a/tests/generated/test_document_ai.py b/tests/generated/test_document_ai.py new file mode 100644 index 0000000..8fc6021 --- /dev/null +++ b/tests/generated/test_document_ai.py @@ -0,0 +1,99 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``document_ai`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.document_ai.analyze_response import DocumentAiAnalyzeResponse +from tests.harness import Expect, check_operation + + +def test_document_ai_analyze() -> None: + """``POST /v3/document-ai/``""" + check_operation( + Expect( + method="POST", + path="/v3/document-ai/", + api="verification", + auth="api_key", + multipart_fields=["fields"], + multipart_files=["document"], + status=200, + response_json={ + "request_id": "9d6a3b1e-2f0c-4a8e-9b7d-1c2e3f4a5b6c", + "document_ai": { + "status": "Approved", + "extracted_data": { + "employer_name": "Acme Payments Ltd", + "employee_name": "Sophia Martinez", + "net_pay": 3120.5, + "pay_date": "2026-07-31", + }, + "detected_codes": [ + { + "type": "CODE_128", + "page_number": 1, + "position": [[96, 456], [96, 616], [1012, 611], [1015, 456]], + "x": 96, + "y": 456, + "width": 919, + "height": 160, + "page_width": 1200, + "page_height": 1700, + "raw_payload": "SU5WLTIwMjYtMDA0Mg==", + "parsed_payload": "INV-2026-0042", + } + ], + "fields": [ + { + "key": "employer_name", + "name": "Employer name", + "instruction": "The employer or company issuing the payslip.", + "type": "text", + "required": True, + "is_full_name": False, + }, + { + "key": "employee_name", + "name": "Employee name", + "instruction": "The full name of the employee as printed on the payslip.", + "type": "text", + "required": True, + "is_full_name": True, + }, + { + "key": "net_pay", + "name": "Net pay", + "instruction": "The net amount paid to the employee for the period.", + "type": "number", + "required": True, + "is_full_name": False, + }, + { + "key": "pay_date", + "name": "Pay date", + "instruction": "The payment date shown on the payslip.", + "type": "date", + "required": False, + "is_full_name": False, + }, + ], + "name_match_score": 100.0, + "name_on_document": "Sophia Martinez", + "warnings": [], + }, + "vendor_data": "user-123", + "metadata": {"flow": "onboarding"}, + "created_at": "2026-08-19T16:40:12.318Z", + }, + ), + lambda c: c.document_ai.analyze( + document=b"file-content", + fields='[{"key": "employer_name", "name": "Employer name", "instruction": "The employer or company issuing the payslip.", "type": "text", "required": true, "is_full_name": false}, {"key": "employee_name", "name": "Employee name", "instruction": "The full name of the employee as printed on the payslip.", "type": "text", "required": true, "is_full_name": true}, {"key": "net_pay", "name": "Net pay", "instruction": "The net amount paid to the employee for the period.", "type": "number", "required": true, "is_full_name": false}, {"key": "pay_date", "name": "Pay date", "instruction": "The payment date shown on the payslip.", "type": "date", "required": false, "is_full_name": false}]', + ), + lambda c: c.document_ai.analyze( + document=b"file-content", + fields='[{"key": "employer_name", "name": "Employer name", "instruction": "The employer or company issuing the payslip.", "type": "text", "required": true, "is_full_name": false}, {"key": "employee_name", "name": "Employee name", "instruction": "The full name of the employee as printed on the payslip.", "type": "text", "required": true, "is_full_name": true}, {"key": "net_pay", "name": "Net pay", "instruction": "The net amount paid to the employee for the period.", "type": "number", "required": true, "is_full_name": false}, {"key": "pay_date", "name": "Pay date", "instruction": "The payment date shown on the payslip.", "type": "date", "required": false, "is_full_name": false}]', + ), + lambda r: isinstance(r, DocumentAiAnalyzeResponse), + lambda r: isinstance(r, DocumentAiAnalyzeResponse), + ) diff --git a/tests/generated/test_email.py b/tests/generated/test_email.py new file mode 100644 index 0000000..0905e1f --- /dev/null +++ b/tests/generated/test_email.py @@ -0,0 +1,164 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``email`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.email_risk_response import EmailRiskResponse +from didit.types.components.email_verification_check_response import EmailVerificationCheckResponse +from didit.types.components.email_verification_send_response import EmailVerificationSendResponse +from tests.harness import Expect, check_operation + + +def test_email_assess_risk() -> None: + """``POST /v3/email/risk/``""" + check_operation( + Expect( + method="POST", + path="/v3/email/risk/", + api="verification", + auth="api_key", + json_body={"email": "alice@example.com"}, + status=200, + response_json={ + "request_id": "82397be9-5921-4970-a979-2d5b43e08b33", + "status": "Approved", + "email": { + "status": "Approved", + "email": "alice@example.com", + "is_breached": False, + "breaches": [], + "is_disposable": False, + "is_undeliverable": False, + "verification_attempts": 0, + "verified_at": None, + "warnings": [], + "lifecycle": [], + "matches": [], + "email_intelligence": { + "score": 4.0, + "deliverable": "high", + "breach_risk_level": "Low", + "breach_risk_index": 4, + "is_recent_breach": False, + "is_frequent_breached": False, + "is_disposable": False, + "is_free_domain": False, + "is_breached": False, + "no_of_breaches": 0, + "reason_codes": [], + }, + "enrichment": { + "score": 4.0, + "deliverable": "high", + "breach_risk_level": "Low", + "breach_risk_index": 4, + "is_recent_breach": False, + "is_frequent_breached": False, + "is_disposable": False, + "is_free_domain": False, + "is_breached": False, + "no_of_breaches": 0, + "reason_codes": [], + }, + }, + "vendor_data": "lead-123", + "metadata": {"source": "signup_prescreen"}, + "created_at": "2026-09-01T10:30:00.123456+00:00", + }, + ), + lambda c: c.email.assess_risk(email="alice@example.com"), + lambda c: c.email.assess_risk(email="alice@example.com"), + lambda r: isinstance(r, EmailRiskResponse), + lambda r: isinstance(r, EmailRiskResponse), + ) + + +def test_email_check_code() -> None: + """``POST /v3/email/check/``""" + check_operation( + Expect( + method="POST", + path="/v3/email/check/", + api="verification", + auth="api_key", + json_body={"email": "alice@example.com", "code": "123456"}, + status=200, + response_json={ + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Approved", + "message": "The verification code is correct.", + "email": { + "status": "Approved", + "email": "alice@example.com", + "is_breached": False, + "breaches": [], + "is_disposable": False, + "is_undeliverable": False, + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [], + "lifecycle": [ + { + "type": "EMAIL_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": {"status": "Success", "reason": None}, + "fee": 0.03, + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": {"code_tried": "123456", "status": "Approved"}, + "fee": 0, + }, + { + "type": "EMAIL_VERIFICATION_APPROVED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": None, + "fee": 0, + }, + ], + "matches": [], + }, + "vendor_data": "user-1234", + "metadata": None, + "created_at": "2026-06-12T01:24:47.401719+00:00", + }, + ), + lambda c: c.email.check_code(email="alice@example.com", code="123456"), + lambda c: c.email.check_code(email="alice@example.com", code="123456"), + lambda r: isinstance(r, EmailVerificationCheckResponse), + lambda r: isinstance(r, EmailVerificationCheckResponse), + ) + + +def test_email_send_code() -> None: + """``POST /v3/email/send/``""" + check_operation( + Expect( + method="POST", + path="/v3/email/send/", + api="verification", + auth="api_key", + json_body={ + "email": "alice@example.com", + "options": {"locale": "en"}, + "vendor_data": "user-1234", + }, + status=200, + response_json={ + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Success", + "reason": None, + "vendor_data": "user-1234", + "metadata": None, + }, + ), + lambda c: c.email.send_code( + email="alice@example.com", options={"locale": "en"}, vendor_data="user-1234" + ), + lambda c: c.email.send_code( + email="alice@example.com", options={"locale": "en"}, vendor_data="user-1234" + ), + lambda r: isinstance(r, EmailVerificationSendResponse), + lambda r: isinstance(r, EmailVerificationSendResponse), + ) diff --git a/tests/generated/test_face_match.py b/tests/generated/test_face_match.py new file mode 100644 index 0000000..4ec78cb --- /dev/null +++ b/tests/generated/test_face_match.py @@ -0,0 +1,83 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``face_match`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.face_match.compare_response import FaceMatchCompareResponse +from tests.harness import Expect, check_operation + + +def test_face_match_compare() -> None: + """``POST /v3/face-match/``""" + check_operation( + Expect( + method="POST", + path="/v3/face-match/", + api="verification", + auth="api_key", + multipart_fields=[ + "face_match_score_decline_threshold", + "metadata", + "rotate_image", + "save_api_request", + "vendor_data", + ], + multipart_files=["ref_image", "user_image"], + status=200, + response_json={ + "request_id": "f9f5a777-8002-44c6-baaf-128bcfd6e226", + "face_match": { + "status": "Approved", + "score": 99.41, + "user_image": { + "entities": [ + { + "bbox": [661, 728, 1688, 2188], + "confidence": 0.732973, + "age": 26.91, + "gender": "male", + "race": None, + } + ], + "best_angle": 0, + }, + "ref_image": { + "entities": [ + { + "bbox": [653, 745, 1691, 2185], + "confidence": 0.722082, + "age": 27.0, + "gender": "male", + "race": None, + } + ], + "best_angle": 0, + }, + "warnings": [], + }, + "vendor_data": "user-123", + "metadata": {"flow": "login"}, + "created_at": "2026-06-12T01:04:42.763237+00:00", + }, + ), + lambda c: c.face_match.compare( + user_image=b"file-content", + ref_image=b"file-content", + face_match_score_decline_threshold=50, + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "login"}, + ), + lambda c: c.face_match.compare( + user_image=b"file-content", + ref_image=b"file-content", + face_match_score_decline_threshold=50, + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "login"}, + ), + lambda r: isinstance(r, FaceMatchCompareResponse), + lambda r: isinstance(r, FaceMatchCompareResponse), + ) diff --git a/tests/generated/test_face_search.py b/tests/generated/test_face_search.py new file mode 100644 index 0000000..78fd262 --- /dev/null +++ b/tests/generated/test_face_search.py @@ -0,0 +1,96 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``face_search`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.face_search.search_response import FaceSearchSearchResponse +from tests.harness import Expect, check_operation + + +def test_face_search_search() -> None: + """``POST /v3/face-search/``""" + check_operation( + Expect( + method="POST", + path="/v3/face-search/", + api="verification", + auth="api_key", + multipart_fields=[ + "metadata", + "rotate_image", + "save_api_request", + "search_type", + "vendor_data", + ], + multipart_files=["user_image"], + status=200, + response_json={ + "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", + "face_search": { + "status": "Declined", + "total_matches": 1, + "matches": [ + { + "session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", + "session_number": 323442, + "similarity_percentage": 99.99, + "source": "session", + "vendor_data": "user-1", + "verification_date": "2025-01-01T00:00:00Z", + "user_details": { + "full_name": "Jane Marie Doe", + "document_type": "ID", + "document_number": "X1234567", + }, + "match_image_url": "https:///face/3f6a1c2e/reference.jpg", + "status": "Approved", + "is_blocklisted": True, + "is_allowlisted": False, + "api_service": None, + "vendor_user_id": "2f7c1a9e-8b4d-4c6a-9e1f-3d5b7a9c1e2f", + "biometric_template_id": None, + } + ], + "user_image": { + "entities": [{"bbox": [40, 40, 120, 120], "confidence": 0.732973}], + "best_angle": 0, + }, + "warnings": [ + { + "risk": "FACE_IN_BLOCKLIST", + "feature": "LIVENESS", + "additional_data": { + "blocklisted_session_id": "882c42d5-8a4d-4d20-8080-a22f57822c86", + "blocklisted_session_number": 323442, + "api_service": None, + }, + "log_type": "error", + "short_description": "Face in blocklist", + "long_description": "The system identified a face in the blocklist, which means the face is not allowed to be verified.", + } + ], + }, + "vendor_data": "user-123", + "metadata": None, + "created_at": "2026-06-12T01:04:42.763237+00:00", + }, + ), + lambda c: c.face_search.search( + user_image=b"file-content", + search_type="most_similar", + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "dedup_check"}, + ), + lambda c: c.face_search.search( + user_image=b"file-content", + search_type="most_similar", + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "dedup_check"}, + ), + lambda r: isinstance(r, FaceSearchSearchResponse), + lambda r: isinstance(r, FaceSearchSearchResponse), + ) diff --git a/tests/generated/test_fraud_checks.py b/tests/generated/test_fraud_checks.py new file mode 100644 index 0000000..fdf687a --- /dev/null +++ b/tests/generated/test_fraud_checks.py @@ -0,0 +1,208 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``fraud_checks`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.fraud_check_plan import FraudCheckPlan +from didit.types.components.fraud_check_response import FraudCheckResponse +from tests.harness import Expect, check_operation + + +def test_fraud_checks_create() -> None: + """``POST /v3/fraud/checks/``""" + check_operation( + Expect( + method="POST", + path="/v3/fraud/checks/", + api="verification", + auth="api_key", + json_body={"subject": {"identifiers": [{"type": "email", "value": "sample"}]}}, + status=200, + response_json={ + "check_id": "00000000-0000-4000-8000-000000000003", + "request_id": "00000000-0000-4000-8000-000000000003", + "client_reference": "sample", + "processing_status": "pending", + "profile_id": "sample", + "profile_version": "sample", + "created_at": "2026-01-01T00:00:00Z", + "completed_at": "2026-01-01T00:00:00Z", + "inputs": [ + { + "reference": "sample", + "type": "email", + "namespace": {}, + "quality": "sample", + "normalization": {}, + "availability": "available", + } + ], + "modules": [ + { + "module": "normalization", + "status": "planned", + "reason": "sample", + "latency_ms": 1, + "freshness": "sample", + "billable_units": 1, + } + ], + "attribute_evidence": [{}], + "compound_evidence": [{}], + "risk": [ + { + "dimension": "identity", + "score": 1, + "band": "low", + "availability": "available", + "requested_but_unavailable": True, + "reason_codes": ["sample"], + "evidence_indexes": [1], + } + ], + "network": {}, + "enrichment": {}, + "identity_relationship": { + "result": "match", + "reference_type": "sample", + "country": "sample", + "availability": "available", + "reason_codes": ["sample"], + }, + "recommendation": "approve", + "next_action": "none", + "decision_reasons": ["sample"], + "coverage": {}, + "usage": [{}], + "vendor_data": "sample", + "metadata": "value", + }, + strict_type=FraudCheckResponse, + ), + lambda c: c.fraud_checks.create( + subject={"identifiers": [{"type": "email", "value": "sample"}]} + ), + lambda c: c.fraud_checks.create( + subject={"identifiers": [{"type": "email", "value": "sample"}]} + ), + lambda r: isinstance(r, FraudCheckResponse), + lambda r: isinstance(r, FraudCheckResponse), + ) + + +def test_fraud_checks_plan() -> None: + """``POST /v3/fraud/checks/plan/``""" + check_operation( + Expect( + method="POST", + path="/v3/fraud/checks/plan/", + api="verification", + auth="api_key", + json_body={"subject": {"identifiers": [{"type": "email", "value": "sample"}]}}, + status=200, + response_json={ + "profile_id": "sample", + "profile_version": "sample", + "modules": [ + { + "module": "normalization", + "available": True, + "availability_reason": "sample", + "will_run": True, + "selection_reason": "sample", + "estimated_price": "sample", + } + ], + "estimated_price": "sample", + "currency": "sample", + "is_sandbox": True, + "can_afford": True, + }, + strict_type=FraudCheckPlan, + ), + lambda c: c.fraud_checks.plan( + subject={"identifiers": [{"type": "email", "value": "sample"}]} + ), + lambda c: c.fraud_checks.plan( + subject={"identifiers": [{"type": "email", "value": "sample"}]} + ), + lambda r: isinstance(r, FraudCheckPlan), + lambda r: isinstance(r, FraudCheckPlan), + ) + + +def test_fraud_checks_retrieve() -> None: + """``GET /v3/fraud/checks/{check_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/fraud/checks/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "check_id": "00000000-0000-4000-8000-000000000003", + "request_id": "00000000-0000-4000-8000-000000000003", + "client_reference": "sample", + "processing_status": "pending", + "profile_id": "sample", + "profile_version": "sample", + "created_at": "2026-01-01T00:00:00Z", + "completed_at": "2026-01-01T00:00:00Z", + "inputs": [ + { + "reference": "sample", + "type": "email", + "namespace": {}, + "quality": "sample", + "normalization": {}, + "availability": "available", + } + ], + "modules": [ + { + "module": "normalization", + "status": "planned", + "reason": "sample", + "latency_ms": 1, + "freshness": "sample", + "billable_units": 1, + } + ], + "attribute_evidence": [{}], + "compound_evidence": [{}], + "risk": [ + { + "dimension": "identity", + "score": 1, + "band": "low", + "availability": "available", + "requested_but_unavailable": True, + "reason_codes": ["sample"], + "evidence_indexes": [1], + } + ], + "network": {}, + "enrichment": {}, + "identity_relationship": { + "result": "match", + "reference_type": "sample", + "country": "sample", + "availability": "available", + "reason_codes": ["sample"], + }, + "recommendation": "approve", + "next_action": "none", + "decision_reasons": ["sample"], + "coverage": {}, + "usage": [{}], + "vendor_data": "sample", + "metadata": "value", + }, + strict_type=FraudCheckResponse, + ), + lambda c: c.fraud_checks.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.fraud_checks.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, FraudCheckResponse), + lambda r: isinstance(r, FraudCheckResponse), + ) diff --git a/tests/generated/test_id_verification.py b/tests/generated/test_id_verification.py new file mode 100644 index 0000000..76b7f40 --- /dev/null +++ b/tests/generated/test_id_verification.py @@ -0,0 +1,168 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``id_verification`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.id_verification.verify_response import IdVerificationVerifyResponse +from tests.harness import Expect, check_operation + + +def test_id_verification_verify() -> None: + """``POST /v3/id-verification/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/", + api="verification", + auth="api_key", + multipart_fields=[ + "metadata", + "perform_document_liveness", + "save_api_request", + "vendor_data", + ], + multipart_files=["back_image", "front_image"], + status=200, + response_json={ + "request_id": "11d219ed-d59c-4b1d-8d65-9b933f12d5b8", + "id_verification": { + "status": "Approved", + "document_type": "Identity Card", + "document_subtype": "ID_CARD_GENERIC", + "document_number": "CBX164224", + "personal_number": "20446581H", + "portrait_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-portrait_image-8c2f.jpg?signature=...", + "front_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-front_image-4a1e.jpg?signature=...", + "back_image": "https:///ocr/11d219ed-d59c-4b1d-8d65-9b933f12d5b8-back_image-77b0.jpg?signature=...", + "front_image_camera_front": None, + "back_image_camera_front": None, + "front_image_camera_front_face_match_score": None, + "back_image_camera_front_face_match_score": None, + "front_image_quality_score": { + "focus_score": 78.0, + "brightness_score": 83.2, + "brightness_issue": "ok", + "is_document_fully_visible": True, + "resolution_score": 40.7, + "overall_score": 70.2, + }, + "back_image_quality_score": { + "focus_score": 100.0, + "brightness_score": 95.6, + "brightness_issue": "ok", + "is_document_fully_visible": True, + "resolution_score": 43.0, + "overall_score": 84.4, + }, + "date_of_birth": "1980-01-13", + "age": 46, + "expiration_date": "2032-03-31", + "date_of_issue": "2022-03-31", + "issuing_state": "ESP", + "issuing_state_name": "Spain", + "first_name": "Julio Francisco", + "last_name": "Fores Sena", + "full_name": "Julio Francisco Fores Sena", + "gender": "M", + "address": "Brda. Urb. El Cardonal 0052 52 Po3 B,Taco,San Cristobal De La Laguna,Santa Cruz De Tenerife", + "formatted_address": "Av. el Cardonal, 52, b, 38108 La Laguna, Santa Cruz de Tenerife, Spain", + "place_of_birth": "Valencia, Valencia", + "marital_status": "UNKNOWN", + "nationality": "ESP", + "extra_fields": {"first_surname": "Fores", "second_surname": "Sena"}, + "mrz": { + "surname": "FORES SENA", + "name": "JULIO FRANCISCO", + "country": "ESP", + "nationality": "ESP", + "birth_date": "800113", + "expiry_date": "320331", + "sex": "M", + "document_type": "ID", + "document_number": "CBX164224", + "optional_data": "20446581H", + "optional_data_2": "", + "birth_date_hash": "9", + "expiry_date_hash": "8", + "document_number_hash": "3", + "final_hash": "3", + "personal_number": "20446581H", + "warnings": [], + "errors": [], + "mrz_type": "TD1", + "mrz_string": "IDESPCBX164224320446581H<<<<<<\n8001139M3203318ESP<<<<<<<<<<<3\nFORES None: + """``POST /v3/id-verification/wallets/verifications/{request_id}/cancel/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/verifications/00000000-0000-4000-8000-000000000001/cancel/", + api="verification", + auth="api_key", + status=200, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "ftn", + "name": "Finnish Trust Network", + "issuing_authority": "Finnish banks and mobile operators (FTN)", + }, + "country": "FIN", + "status": "Declined", + "outcome": "cancelled", + "error": "cancelled_by_customer", + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": None, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.cancel_verification("00000000-0000-4000-8000-000000000001"), + lambda c: c.id_wallets.cancel_verification("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_list() -> None: + """``GET /v3/id-verification/wallets/``""" + check_operation( + Expect( + method="GET", + path="/v3/id-verification/wallets/", + api="verification", + auth="api_key", + status=200, + response_json={ + "wallets": [ + { + "id": "sample", + "name": "sample", + "path": "sample", + "countries": ["sample"], + "issuing_authority": "sample", + "level_of_assurance": "sample", + "interaction": "redirect", + "required_fields": ["sample"], + "available": True, + } + ] + }, + strict_type=DigitalIdWalletApiWalletCatalogResponse, + ), + lambda c: c.id_wallets.list(), + lambda c: c.id_wallets.list(), + lambda r: isinstance(r, DigitalIdWalletApiWalletCatalogResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletCatalogResponse), + ) + + +def test_id_wallets_poll_verification() -> None: + """``POST /v3/id-verification/wallets/verifications/{request_id}/poll/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/verifications/00000000-0000-4000-8000-000000000001/poll/", + api="verification", + auth="api_key", + status=200, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "smart_id", + "name": "Smart-ID", + "issuing_authority": "SK ID Solutions", + }, + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "in_app", + "simulated": True, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "confirm_in_app", + "verification_code": "1234", + "poll_interval_seconds": 2, + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.poll_verification("00000000-0000-4000-8000-000000000001"), + lambda c: c.id_wallets.poll_verification("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_retrieve_verification() -> None: + """``GET /v3/id-verification/wallets/verifications/{request_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/id-verification/wallets/verifications/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "ftn", + "name": "Finnish Trust Network", + "issuing_authority": "Finnish banks and mobile operators (FTN)", + }, + "country": "FIN", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.retrieve_verification("00000000-0000-4000-8000-000000000001"), + lambda c: c.id_wallets.retrieve_verification("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_audkenni() -> None: + """``POST /v3/id-verification/wallets/audkenni/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/audkenni/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "ISL", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "audkenni", + "name": "Auðkenni", + "issuing_authority": "Auðkenni (Icelandic electronic ID)", + }, + "country": "ISL", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_audkenni( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="ISL", + ), + lambda c: c.id_wallets.start_audkenni( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="ISL", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_bankid_cz() -> None: + """``POST /v3/id-verification/wallets/bankid-cz/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/bankid-cz/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "CZE", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "bankid_cz", + "name": "Bank iD", + "issuing_authority": "Bankovní identita, a.s.", + }, + "country": "CZE", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_bankid_cz( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="CZE", + ), + lambda c: c.id_wallets.start_bankid_cz( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="CZE", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_bankid_no() -> None: + """``POST /v3/id-verification/wallets/bankid-no/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/bankid-no/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "NOR", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "bankid_no", + "name": "BankID", + "issuing_authority": "BankID BankAxept AS", + }, + "country": "NOR", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_bankid_no( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NOR", + ), + lambda c: c.id_wallets.start_bankid_no( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NOR", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_bankid_se() -> None: + """``POST /v3/id-verification/wallets/bankid-se/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/bankid-se/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "SWE", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "bankid_se", + "name": "BankID", + "issuing_authority": "Finansiell ID-Teknik / bank consortium", + }, + "country": "SWE", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_bankid_se( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="SWE", + ), + lambda c: c.id_wallets.start_bankid_se( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="SWE", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_buypass() -> None: + """``POST /v3/id-verification/wallets/buypass/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/buypass/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "NOR", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "buypass", + "name": "Buypass ID", + "issuing_authority": "Buypass AS", + }, + "country": "NOR", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_buypass( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NOR", + ), + lambda c: c.id_wallets.start_buypass( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NOR", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_connectid() -> None: + """``POST /v3/id-verification/wallets/connectid/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/connectid/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "AUS", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "connectid", + "name": "ConnectID", + "issuing_authority": "Australian Payments Plus", + }, + "country": "AUS", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_connectid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="AUS", + ), + lambda c: c.id_wallets.start_connectid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="AUS", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_diia() -> None: + """``POST /v3/id-verification/wallets/diia/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/diia/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "UKR", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "diia", + "name": "Diia", + "issuing_authority": "Ministry of Digital Transformation of Ukraine", + }, + "country": "UKR", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_diia( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="UKR", + ), + lambda c: c.id_wallets.start_diia( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="UKR", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_eparaksts_mobile() -> None: + """``POST /v3/id-verification/wallets/eparaksts-mobile/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/eparaksts-mobile/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "LVA", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "eparaksts_mobile", + "name": "eParaksts mobile", + "issuing_authority": "LVRTC (Latvian State Radio and Television Centre)", + }, + "country": "LVA", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_eparaksts_mobile( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="LVA", + ), + lambda c: c.id_wallets.start_eparaksts_mobile( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="LVA", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_eudi() -> None: + """``POST /v3/id-verification/wallets/eudi/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/eudi/", + api="verification", + auth="api_key", + json_body={ + "country": "AUT", + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "eudi", + "name": "EUDI Wallet", + "issuing_authority": "The user's own member state; the issuer differs per country", + }, + "country": "AUT", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_eudi( + country="AUT", + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda c: c.id_wallets.start_eudi( + country="AUT", + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_franceconnect() -> None: + """``POST /v3/id-verification/wallets/franceconnect/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/franceconnect/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "FRA", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "franceconnect", + "name": "FranceConnect", + "issuing_authority": "DINUM (French state)", + }, + "country": "FRA", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_franceconnect( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="FRA", + ), + lambda c: c.id_wallets.start_franceconnect( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="FRA", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_frejaid() -> None: + """``POST /v3/id-verification/wallets/frejaid/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/frejaid/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "SWE", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "frejaid", + "name": "Freja eID", + "issuing_authority": "Freja eID Group", + }, + "country": "SWE", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_frejaid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="SWE", + ), + lambda c: c.id_wallets.start_frejaid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="SWE", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_ftn() -> None: + """``POST /v3/id-verification/wallets/ftn/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/ftn/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "FIN", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "ftn", + "name": "Finnish Trust Network", + "issuing_authority": "Finnish banks and mobile operators (FTN)", + }, + "country": "FIN", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_ftn( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="FIN", + ), + lambda c: c.id_wallets.start_ftn( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="FIN", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_govbr() -> None: + """``POST /v3/id-verification/wallets/govbr/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/govbr/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "BRA", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "govbr", + "name": "gov.br", + "issuing_authority": "Governo Federal do Brasil", + }, + "country": "BRA", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_govbr( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="BRA", + ), + lambda c: c.id_wallets.start_govbr( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="BRA", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_govuk_wallet() -> None: + """``POST /v3/id-verification/wallets/govuk-wallet/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/govuk-wallet/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "GBR", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "govuk_wallet", + "name": "GOV.UK Wallet", + "issuing_authority": "UK Government Digital Service", + }, + "country": "GBR", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_govuk_wallet( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="GBR", + ), + lambda c: c.id_wallets.start_govuk_wallet( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="GBR", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_idcard_ee() -> None: + """``POST /v3/id-verification/wallets/idcard-ee/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/idcard-ee/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "EST", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "idcard_ee", + "name": "Estonian ID-card", + "issuing_authority": "Police and Border Guard Board (Estonia)", + }, + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_idcard_ee( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="EST", + ), + lambda c: c.id_wallets.start_idcard_ee( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="EST", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_idin() -> None: + """``POST /v3/id-verification/wallets/idin/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/idin/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "NLD", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "idin", + "name": "iDIN", + "issuing_authority": "Dutch banks (Currence iDIN)", + }, + "country": "NLD", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_idin( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NLD", + ), + lambda c: c.id_wallets.start_idin( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NLD", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_itsme() -> None: + """``POST /v3/id-verification/wallets/itsme/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/itsme/", + api="verification", + auth="api_key", + json_body={ + "country": "BEL", + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "itsme", + "name": "itsme", + "issuing_authority": "Belgian Mobile ID", + }, + "country": "BEL", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_itsme( + country="BEL", + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda c: c.id_wallets.start_itsme( + country="BEL", + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_mitid() -> None: + """``POST /v3/id-verification/wallets/mitid/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/mitid/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "DNK", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "mitid", + "name": "MitID", + "issuing_authority": "Danish Agency for Digital Government", + }, + "country": "DNK", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_mitid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="DNK", + ), + lambda c: c.id_wallets.start_mitid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="DNK", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_mobile_id() -> None: + """``POST /v3/id-verification/wallets/mobile-id/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/mobile-id/", + api="verification", + auth="api_key", + json_body={ + "country": "EST", + "personal_code": "00000000000", + "phone_number": "+37200000000", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "mobile_id", + "name": "Mobile-ID", + "issuing_authority": "SK ID Solutions with the national mobile operators", + }, + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "in_app", + "simulated": True, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "confirm_in_app", + "verification_code": "1234", + "poll_interval_seconds": 2, + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_mobile_id( + country="EST", + personal_code="00000000000", + phone_number="+37200000000", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda c: c.id_wallets.start_mobile_id( + country="EST", + personal_code="00000000000", + phone_number="+37200000000", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_mojeid() -> None: + """``POST /v3/id-verification/wallets/mojeid/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/mojeid/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "CZE", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": {"id": "mojeid", "name": "MojeID", "issuing_authority": "CZ.NIC"}, + "country": "CZE", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_mojeid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="CZE", + ), + lambda c: c.id_wallets.start_mojeid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="CZE", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_oneid() -> None: + """``POST /v3/id-verification/wallets/oneid/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/oneid/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "GBR", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "oneid", + "name": "OneID", + "issuing_authority": "OneID (UK bank-verified identity)", + }, + "country": "GBR", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_oneid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="GBR", + ), + lambda c: c.id_wallets.start_oneid( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="GBR", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_personalausweis() -> None: + """``POST /v3/id-verification/wallets/personalausweis/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/personalausweis/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "DEU", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "personalausweis", + "name": "Personalausweis", + "issuing_authority": "Bundesministerium des Innern (eID)", + }, + "country": "DEU", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_personalausweis( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="DEU", + ), + lambda c: c.id_wallets.start_personalausweis( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="DEU", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_smart_id() -> None: + """``POST /v3/id-verification/wallets/smart-id/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/smart-id/", + api="verification", + auth="api_key", + json_body={ + "country": "EST", + "personal_code": "00000000000", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "smart_id", + "name": "Smart-ID", + "issuing_authority": "SK ID Solutions", + }, + "country": "EST", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "in_app", + "simulated": True, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "confirm_in_app", + "verification_code": "1234", + "poll_interval_seconds": 2, + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_smart_id( + country="EST", + personal_code="00000000000", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda c: c.id_wallets.start_smart_id( + country="EST", + personal_code="00000000000", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_uae_pass() -> None: + """``POST /v3/id-verification/wallets/uae-pass/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/uae-pass/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "ARE", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "uae_pass", + "name": "UAE PASS", + "issuing_authority": "UAE Digital Government Authority", + }, + "country": "ARE", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_uae_pass( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="ARE", + ), + lambda c: c.id_wallets.start_uae_pass( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="ARE", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) + + +def test_id_wallets_start_vipps() -> None: + """``POST /v3/id-verification/wallets/vipps/``""" + check_operation( + Expect( + method="POST", + path="/v3/id-verification/wallets/vipps/", + api="verification", + auth="api_key", + json_body={ + "return_url": "https://app.example.com/identity/complete", + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "country": "NOR", + }, + status=201, + response_json={ + "request_id": "00000000-0000-4000-8000-000000000001", + "wallet_verification": { + "wallet": { + "id": "vipps", + "name": "Vipps", + "issuing_authority": "Vipps MobilePay / BankID NO", + }, + "country": "NOR", + "status": "Not Finished", + "outcome": "pending", + "error": None, + "interaction": "redirect", + "simulated": False, + "expires_at": "2026-01-01T10:10:00Z", + "next_action": { + "type": "redirect", + "url": "https://wallet.example.com/authorize", + }, + "identity": None, + "attributes": None, + "level_of_assurance": None, + "verified_at": None, + "signature_valid": None, + "credential_type": "sample-secret", + }, + "vendor_data": "your-customer-reference", + "metadata": {"reference": "example"}, + "created_at": "2026-01-01T10:00:00Z", + }, + ), + lambda c: c.id_wallets.start_vipps( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NOR", + ), + lambda c: c.id_wallets.start_vipps( + return_url="https://app.example.com/identity/complete", + vendor_data="your-customer-reference", + metadata={"reference": "example"}, + country="NOR", + ), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + lambda r: isinstance(r, DigitalIdWalletApiWalletVerificationResponse), + ) diff --git a/tests/generated/test_ip.py b/tests/generated/test_ip.py new file mode 100644 index 0000000..c7c2769 --- /dev/null +++ b/tests/generated/test_ip.py @@ -0,0 +1,56 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``ip`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.ip_risk_response import IPRiskResponse +from tests.harness import Expect, check_operation + + +def test_ip_assess_risk() -> None: + """``POST /v3/ip/risk/``""" + check_operation( + Expect( + method="POST", + path="/v3/ip/risk/", + api="verification", + auth="api_key", + json_body={"ip_address": "8.8.8.8"}, + status=200, + response_json={ + "request_id": "9a96ed2a-721e-4b86-8cbf-d61eda529279", + "status": "Approved", + "ip": { + "status": "Approved", + "ip_address": "192.0.2.10", + "ip_country": "Spain", + "ip_country_code": "ES", + "ip_state": "Madrid", + "ip_city": "Madrid", + "latitude": 40.4168, + "longitude": -3.7038, + "isp": "Example Telecom", + "organization": "Example Telecom SA", + "is_vpn_or_tor": False, + "is_data_center": False, + "time_zone": "Europe/Madrid", + "time_zone_offset": "+0200", + "asn_number": 3352, + "asn_organization": "EXAMPLE-TELECOM", + "connection_type": "broadband", + "carrier": "Example Telecom", + "proxy_type": None, + "claimed_country": "ESP", + "country_mismatch": False, + "threat": {}, + }, + "vendor_data": "lead-123", + "metadata": None, + "created_at": "2026-09-01T10:30:00.123456+00:00", + }, + ), + lambda c: c.ip.assess_risk(ip_address="8.8.8.8"), + lambda c: c.ip.assess_risk(ip_address="8.8.8.8"), + lambda r: isinstance(r, IPRiskResponse), + lambda r: isinstance(r, IPRiskResponse), + ) diff --git a/tests/generated/test_kyb.py b/tests/generated/test_kyb.py new file mode 100644 index 0000000..0c0596f --- /dev/null +++ b/tests/generated/test_kyb.py @@ -0,0 +1,416 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``kyb`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.kyb.list_shareholders_response import ( + KybListShareholdersResponse, + KybListShareholdersResponse202, +) +from didit.types.kyb.list_ubos_response import KybListUbosResponse, KybListUbosResponse202 +from didit.types.kyb.search_response import KybSearchResponse +from didit.types.kyb.select_response import KybSelectResponse +from tests.harness import Expect, check_operation + + +def test_kyb_list_shareholders() -> None: + """``POST /v3/kyb/shareholders/``""" + check_operation( + Expect( + method="POST", + path="/v3/kyb/shareholders/", + api="verification", + auth="api_key", + json_body={"kyb_response_id": "69aeeb95febb0f1704042259"}, + status=200, + response_json={ + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", + "node_id": "feature_kyb_registry", + "status": "In Review", + "registry_status": "active", + "data_resolved": True, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": "", + "company_type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "tax_number": "GB412 8925 93", + "risk_level": "low risk", + "verification_status": "verified", + "is_from_registry": True, + "fetch_status": "resolved", + "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", + "nature_of_business": None, + "registered_capital": None, + "registered_capital_amount": None, + "registered_capital_currency": None, + "website": "https://www.tesco.com/", + "email": "customer.service@tesco.co.uk", + "phone": "03301231688", + "legal_entity_identifier": None, + "location_of_registration": None, + "financial_summary": { + "currency": "Pound (£)", + "total_number_of_shares": "8174378721", + "meta_detail": {"Class_Of_Share": "Ordinary"}, + }, + "officers": [ + { + "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", + "name": "CHRISTOPHER JON TAYLOR", + "designation": "Secretary", + "role": "secretary", + "nationality": None, + "is_active": True, + "kyc_status": None, + "kyc_session_url": None, + }, + { + "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", + "name": "MELISSA BETHELL", + "designation": "Director", + "role": "director", + "nationality": "British", + "is_active": True, + "kyc_status": None, + "kyc_session_url": None, + }, + ], + "beneficial_owners": [], + "addresses": [ + { + "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "type": "legal_entity_registry_address", + "description": "registered office address", + } + ], + "industries": None, + "accounts": { + "last_account": "26 February 2025", + "next_account": "26 February 2026", + "due_by_date": "26 August 2026", + }, + "registry_data": { + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "tax_number": "GB412 8925 93", + "country_code": "GB", + "fetch_status": "resolved", + "industries_detail": [ + { + "code": "47110", + "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", + "description": None, + "meta_detail": [], + } + ], + }, + "user_provided_data": None, + "confirmed_by_user_at": None, + "last_console_edit_at": None, + "is_editable": True, + }, + "vendor_data": "company-1234", + "metadata": None, + "created_at": "2026-06-11T10:45:00.000000+00:00", + }, + ), + lambda c: c.kyb.list_shareholders(kyb_response_id="69aeeb95febb0f1704042259"), + lambda c: c.kyb.list_shareholders(kyb_response_id="69aeeb95febb0f1704042259"), + lambda r: ( + (isinstance(r, KybListShareholdersResponse)) + or (isinstance(r, KybListShareholdersResponse202)) + ), + lambda r: ( + (isinstance(r, KybListShareholdersResponse)) + or (isinstance(r, KybListShareholdersResponse202)) + ), + ) + + +def test_kyb_list_ubos() -> None: + """``POST /v3/kyb/ubo/``""" + check_operation( + Expect( + method="POST", + path="/v3/kyb/ubo/", + api="verification", + auth="api_key", + json_body={"kyb_response_id": "69aeeb95febb0f1704042259"}, + status=200, + response_json={ + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", + "node_id": "feature_kyb_registry", + "status": "In Review", + "registry_status": "active", + "data_resolved": True, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": "", + "company_type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "tax_number": "GB412 8925 93", + "risk_level": "low risk", + "verification_status": "verified", + "is_from_registry": True, + "fetch_status": "resolved", + "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", + "nature_of_business": None, + "registered_capital": None, + "registered_capital_amount": None, + "registered_capital_currency": None, + "website": "https://www.tesco.com/", + "email": "customer.service@tesco.co.uk", + "phone": "03301231688", + "legal_entity_identifier": None, + "location_of_registration": None, + "financial_summary": { + "currency": "Pound (£)", + "total_number_of_shares": "8174378721", + "meta_detail": {"Class_Of_Share": "Ordinary"}, + }, + "officers": [ + { + "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", + "name": "CHRISTOPHER JON TAYLOR", + "designation": "Secretary", + "role": "secretary", + "nationality": None, + "is_active": True, + "kyc_status": None, + "kyc_session_url": None, + }, + { + "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", + "name": "MELISSA BETHELL", + "designation": "Director", + "role": "director", + "nationality": "British", + "is_active": True, + "kyc_status": None, + "kyc_session_url": None, + }, + ], + "beneficial_owners": [], + "addresses": [ + { + "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "type": "legal_entity_registry_address", + "description": "registered office address", + } + ], + "industries": None, + "accounts": { + "last_account": "26 February 2025", + "next_account": "26 February 2026", + "due_by_date": "26 August 2026", + }, + "registry_data": { + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "tax_number": "GB412 8925 93", + "country_code": "GB", + "fetch_status": "resolved", + "industries_detail": [ + { + "code": "47110", + "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", + "description": None, + "meta_detail": [], + } + ], + }, + "user_provided_data": None, + "confirmed_by_user_at": None, + "last_console_edit_at": None, + "is_editable": True, + }, + "vendor_data": "company-1234", + "metadata": None, + "created_at": "2026-06-11T10:45:00.000000+00:00", + }, + ), + lambda c: c.kyb.list_ubos(kyb_response_id="69aeeb95febb0f1704042259"), + lambda c: c.kyb.list_ubos(kyb_response_id="69aeeb95febb0f1704042259"), + lambda r: (isinstance(r, KybListUbosResponse)) or (isinstance(r, KybListUbosResponse202)), + lambda r: (isinstance(r, KybListUbosResponse)) or (isinstance(r, KybListUbosResponse202)), + ) + + +def test_kyb_search() -> None: + """``POST /v3/kyb/search/``""" + check_operation( + Expect( + method="POST", + path="/v3/kyb/search/", + api="verification", + auth="api_key", + json_body={"country_code": "GB"}, + status=200, + response_json={ + "request_id": "0c7e9a40-3a13-4f7e-8a44-9d2e2f1c5b6a", + "kyb_registry": { + "companies": [ + { + "kyb_response_id": "69aeeb95febb0f1704042259", + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "risk_level": None, + "fetch_status": "pending", + }, + { + "kyb_response_id": "69aeeb95febb0f170404225a", + "name": "E-TESCO LTD", + "registration_number": "15168476", + "status": "active", + "type": "Private Limited Company", + "risk_level": None, + "fetch_status": "pending", + }, + ], + "pagination": {"total": 2, "page": 1, "per_page": 25}, + "search_status": "resolved", + "search_resolved": True, + }, + "vendor_data": "company-1234", + "metadata": None, + "created_at": "2026-06-11T10:40:00.000000+00:00", + }, + ), + lambda c: c.kyb.search(country_code="GB"), + lambda c: c.kyb.search(country_code="GB"), + lambda r: isinstance(r, KybSearchResponse), + lambda r: isinstance(r, KybSearchResponse), + ) + + +def test_kyb_select() -> None: + """``POST /v3/kyb/select/``""" + check_operation( + Expect( + method="POST", + path="/v3/kyb/select/", + api="verification", + auth="api_key", + json_body={"kyb_response_id": "69aeeb95febb0f1704042259"}, + status=200, + response_json={ + "request_id": "9d4f2c7a-1b3e-4a5c-8d6f-0e9a8b7c6d5e", + "kyb_registry": { + "uuid": "f1e2d3c4-b5a6-4789-9c0d-1e2f3a4b5c6d", + "node_id": "feature_kyb_registry", + "status": "In Review", + "registry_status": "active", + "data_resolved": True, + "company_name": "Tesco PLC", + "registration_number": "00445790", + "country_code": "GB", + "region": "", + "company_type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "registered_address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "tax_number": "GB412 8925 93", + "risk_level": "low risk", + "verification_status": "verified", + "is_from_registry": True, + "fetch_status": "resolved", + "alternative_names": "TESCO STORES (HOLDINGS) PUBLIC LIMITED COMPANY", + "nature_of_business": None, + "registered_capital": None, + "registered_capital_amount": None, + "registered_capital_currency": None, + "website": "https://www.tesco.com/", + "email": "customer.service@tesco.co.uk", + "phone": "03301231688", + "legal_entity_identifier": None, + "location_of_registration": None, + "financial_summary": { + "currency": "Pound (£)", + "total_number_of_shares": "8174378721", + "meta_detail": {"Class_Of_Share": "Ordinary"}, + }, + "officers": [ + { + "uuid": "a1b2c3d4-e5f6-4789-9a0b-1c2d3e4f5a6b", + "name": "CHRISTOPHER JON TAYLOR", + "designation": "Secretary", + "role": "secretary", + "nationality": None, + "is_active": True, + "kyc_status": None, + "kyc_session_url": None, + }, + { + "uuid": "b2c3d4e5-f6a7-4890-8b1c-2d3e4f5a6b7c", + "name": "MELISSA BETHELL", + "designation": "Director", + "role": "director", + "nationality": "British", + "is_active": True, + "kyc_status": None, + "kyc_session_url": None, + }, + ], + "beneficial_owners": [], + "addresses": [ + { + "address": "Tesco House, Shire Park, Kestrel Way, Welwyn Garden City, United Kingdom, AL7 1GA", + "type": "legal_entity_registry_address", + "description": "registered office address", + } + ], + "industries": None, + "accounts": { + "last_account": "26 February 2025", + "next_account": "26 February 2026", + "due_by_date": "26 August 2026", + }, + "registry_data": { + "name": "Tesco PLC", + "registration_number": "00445790", + "status": "active", + "type": "Public Limited Company", + "incorporation_date": "1947-11-27", + "tax_number": "GB412 8925 93", + "country_code": "GB", + "fetch_status": "resolved", + "industries_detail": [ + { + "code": "47110", + "name": "Retail Sale In Non-specialised Stores With Food, Beverages Or Tobacco Predominating", + "description": None, + "meta_detail": [], + } + ], + }, + "user_provided_data": None, + "confirmed_by_user_at": None, + "last_console_edit_at": None, + "is_editable": True, + }, + "vendor_data": "company-1234", + "metadata": None, + "created_at": "2026-06-11T10:45:00.000000+00:00", + }, + ), + lambda c: c.kyb.select(kyb_response_id="69aeeb95febb0f1704042259"), + lambda c: c.kyb.select(kyb_response_id="69aeeb95febb0f1704042259"), + lambda r: isinstance(r, KybSelectResponse), + lambda r: isinstance(r, KybSelectResponse), + ) diff --git a/tests/generated/test_networks.py b/tests/generated/test_networks.py new file mode 100644 index 0000000..68ac226 --- /dev/null +++ b/tests/generated/test_networks.py @@ -0,0 +1,497 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``networks`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.network_detail import NetworkDetail +from didit.types.components.network_graph import NetworkGraph +from didit.types.components.network_list_item import NetworkListItem +from didit.types.components.network_map import NetworkMap +from didit.types.components.network_note import NetworkNote +from didit.types.networks.list_members_response import NetworksListMembersResponse +from didit.types.networks.list_signals_response import NetworksListSignalsResponse +from didit.types.networks.retrieve_timeline_response import NetworksRetrieveTimelineResponse +from tests.harness import BINARY_BODY, Expect, check_operation + + +def test_networks_download_report() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/report/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/00000000-0000-4000-8000-000000000001/report/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.networks.download_report("00000000-0000-4000-8000-000000000001"), + lambda c: c.networks.download_report("00000000-0000-4000-8000-000000000001"), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_networks_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/", + api="verification", + auth="api_key", + query={ + "status": ["sample"], + "signal_type": ["sample"], + "pattern_type": ["sample"], + "risk_band": ["sample"], + "min_size": ["1"], + "max_size": ["1"], + "date_from": ["2026-01-01"], + "date_to": ["2026-01-01"], + "q": ["sample"], + "ordering": ["sample"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": 1, + "previous": 1, + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {"score": 1, "band": "low", "bar": 1.5}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + } + ], + "kpis": { + "active": {}, + "in_review": {}, + "applicants_involved": {}, + "detected_this_period": {}, + "false_positive_rate": {}, + }, + }, + ), + lambda c: c.networks.list( + status="sample", + signal_type="sample", + pattern_type="sample", + risk_band="sample", + min_size=1, + max_size=1, + date_from="2026-01-01", + date_to="2026-01-01", + q="sample", + ordering="sample", + limit=1, + offset=1, + ), + lambda c: c.networks.list( + status="sample", + signal_type="sample", + pattern_type="sample", + risk_band="sample", + min_size=1, + max_size=1, + date_from="2026-01-01", + date_to="2026-01-01", + q="sample", + ordering="sample", + limit=1, + offset=1, + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, NetworkListItem) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, NetworkListItem) for x1 in r.results) + ), + ) + + +def test_networks_list_members() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/members/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/members/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "subject": {}, + "outcome_status": "sample", + "node_status": "approved", + "tags": [ + { + "id": "sample", + "name": "sample", + "color": "sample", + "source": "subject_tag", + "category": "customer", + } + ], + "risk": {"score": 1, "band": "low", "bar": 1.5}, + "signal_coverage": {}, + "shared_infrastructure": {}, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + "provenance": { + "session_ids": ["00000000-0000-4000-8000-000000000007"], + "business_session_ids": ["00000000-0000-4000-8000-000000000007"], + "transaction_ids": ["00000000-0000-4000-8000-000000000007"], + }, + } + ] + }, + strict_type=NetworksListMembersResponse, + ), + lambda c: c.networks.list_members("n3a5e000-0000-4000-8000-000000000001"), + lambda c: c.networks.list_members("n3a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, NetworksListMembersResponse), + lambda r: isinstance(r, NetworksListMembersResponse), + ) + + +def test_networks_list_signals() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/signals/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/signals/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "signal_type": "ip_address", + "subject_count": 1, + "coverage": 1.5, + "is_hub": True, + "is_datacenter_ip": True, + "is_high_collision_device": True, + "node_status": "signal_active", + "tags": [ + { + "id": "sample", + "name": "sample", + "color": "sample", + "source": "subject_tag", + "category": "customer", + } + ], + "risk_evidence": {}, + "first_seen_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=NetworksListSignalsResponse, + ), + lambda c: c.networks.list_signals("n3a5e000-0000-4000-8000-000000000001"), + lambda c: c.networks.list_signals("n3a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, NetworksListSignalsResponse), + lambda r: isinstance(r, NetworksListSignalsResponse), + ) + + +def test_networks_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000004", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {"score": 1, "band": "low", "bar": 1.5}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + "stats": {}, + "fraud_tags": [{}], + "risk_signals": {}, + "reviewed_at": "2026-01-01T00:00:00Z", + "reviewed_by": "sample", + "review_reason": "sample", + "notes_count": 1, + }, + strict_type=NetworkDetail, + ), + lambda c: c.networks.retrieve("n3a5e000-0000-4000-8000-000000000001"), + lambda c: c.networks.retrieve("n3a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, NetworkDetail), + lambda r: isinstance(r, NetworkDetail), + ) + + +def test_networks_retrieve_graph() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/graph/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/graph/", + api="verification", + auth="api_key", + query={ + "depth": ["1"], + "focus_kind": ["vendor_user"], + "focus_id": ["00000000-0000-4000-8000-000000000001"], + }, + status=200, + response_json={ + "network": { + "uuid": "00000000-0000-4000-8000-000000000006", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {"score": 1, "band": "low", "bar": 1.5}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + "stats": {}, + "fraud_tags": [{}], + "risk_signals": {}, + "reviewed_at": "2026-01-01T00:00:00Z", + "reviewed_by": "sample", + "review_reason": "sample", + "notes_count": 1, + }, + "depth": 1, + "truncated": True, + "nodes": [{"id": "sample", "kind": "vendor_user"}], + "edges": [ + { + "id": "00000000-0000-4000-8000-000000000006", + "source": "sample", + "target": "sample", + "subject_kind": "vendor_user", + "signal_type": "ip_address", + "confidence": 1.5, + "observation_count": 1, + "first_seen_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ], + "metrics": {"node_count": 1, "edge_count": 1, "hub_signal_count": 1}, + }, + strict_type=NetworkGraph, + ), + lambda c: c.networks.retrieve_graph( + "n3a5e000-0000-4000-8000-000000000001", + depth="1", + focus_kind="vendor_user", + focus_id="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.networks.retrieve_graph( + "n3a5e000-0000-4000-8000-000000000001", + depth="1", + focus_kind="vendor_user", + focus_id="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, NetworkGraph), + lambda r: isinstance(r, NetworkGraph), + ) + + +def test_networks_retrieve_map() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/map/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/map/", + api="verification", + auth="api_key", + status=200, + response_json={ + "network": { + "uuid": "00000000-0000-4000-8000-000000000006", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {"score": 1, "band": "low", "bar": 1.5}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + "stats": {}, + "fraud_tags": [{}], + "risk_signals": {}, + "reviewed_at": "2026-01-01T00:00:00Z", + "reviewed_by": "sample", + "review_reason": "sample", + "notes_count": 1, + }, + "convergence_count": 1, + "countries": ["sample"], + "points": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "country": "sample", + "country_code": "sample", + "city": "sample", + "latitude": 1.5, + "longitude": 1.5, + "observed_at": "2026-01-01T00:00:00Z", + } + ], + }, + strict_type=NetworkMap, + ), + lambda c: c.networks.retrieve_map("n3a5e000-0000-4000-8000-000000000001"), + lambda c: c.networks.retrieve_map("n3a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, NetworkMap), + lambda r: isinstance(r, NetworkMap), + ) + + +def test_networks_retrieve_timeline() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/timeline/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/n3a5e000-0000-4000-8000-000000000001/timeline/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "event_type": "sample", + "occurred_at": "2026-01-01T00:00:00Z", + "label": "sample", + "metadata": {}, + } + ] + }, + strict_type=NetworksRetrieveTimelineResponse, + ), + lambda c: c.networks.retrieve_timeline("n3a5e000-0000-4000-8000-000000000001"), + lambda c: c.networks.retrieve_timeline("n3a5e000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, NetworksRetrieveTimelineResponse), + lambda r: isinstance(r, NetworksRetrieveTimelineResponse), + ) + + +def test_networks_notes_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/00000000-0000-4000-8000-000000000001/notes/", + api="verification", + auth="api_key", + json_body={ + "comment": "sample", + "client_reference": "00000000-0000-4000-8000-000000000001", + }, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "actor_email": "user@example.com", + "comment": "sample", + "client_reference": "00000000-0000-4000-8000-000000000003", + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=NetworkNote, + ), + lambda c: c.networks.notes.create( + "00000000-0000-4000-8000-000000000001", + comment="sample", + client_reference="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.networks.notes.create( + "00000000-0000-4000-8000-000000000001", + comment="sample", + client_reference="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, NetworkNote), + lambda r: isinstance(r, NetworkNote), + ) + + +def test_networks_notes_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/{note_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/00000000-0000-4000-8000-000000000001/notes/00000000-0000-4000-8000-000000000002/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.networks.notes.delete( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.networks.notes.delete( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_networks_notes_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/networks/{network_uuid}/notes/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/networks/00000000-0000-4000-8000-000000000001/notes/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "actor_email": "user@example.com", + "comment": "sample", + "client_reference": "00000000-0000-4000-8000-000000000005", + "created_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.networks.notes.list("00000000-0000-4000-8000-000000000001", limit=1, offset=1), + lambda c: c.networks.notes.list("00000000-0000-4000-8000-000000000001", limit=1, offset=1), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, NetworkNote) for x1 in r.results), + lambda r: isinstance(r, AsyncPage) and all(isinstance(x1, NetworkNote) for x1 in r.results), + ) diff --git a/tests/generated/test_passive_liveness.py b/tests/generated/test_passive_liveness.py new file mode 100644 index 0000000..3f6dda8 --- /dev/null +++ b/tests/generated/test_passive_liveness.py @@ -0,0 +1,72 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``passive_liveness`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.passive_liveness.check_response import PassiveLivenessCheckResponse +from tests.harness import Expect, check_operation + + +def test_passive_liveness_check() -> None: + """``POST /v3/passive-liveness/``""" + check_operation( + Expect( + method="POST", + path="/v3/passive-liveness/", + api="verification", + auth="api_key", + multipart_fields=[ + "face_liveness_score_decline_threshold", + "metadata", + "rotate_image", + "save_api_request", + "vendor_data", + ], + multipart_files=["user_image"], + status=200, + response_json={ + "request_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", + "liveness": { + "status": "Approved", + "method": "PASSIVE", + "score": 95.0, + "user_image": { + "entities": [ + { + "bbox": [661, 728, 1688, 2188], + "confidence": 0.732973, + "age": 26.91, + "gender": "male", + "race": None, + } + ], + "best_angle": 0, + }, + "warnings": [], + "face_quality": 84.21, + "face_luminance": 50.33, + }, + "vendor_data": "user-123", + "metadata": None, + "created_at": "2026-06-12T01:04:42.763237+00:00", + }, + ), + lambda c: c.passive_liveness.check( + user_image=b"file-content", + face_liveness_score_decline_threshold=30, + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "withdrawal"}, + ), + lambda c: c.passive_liveness.check( + user_image=b"file-content", + face_liveness_score_decline_threshold=30, + rotate_image=False, + save_api_request=True, + vendor_data="user-123", + metadata={"flow": "withdrawal"}, + ), + lambda r: isinstance(r, PassiveLivenessCheckResponse), + lambda r: isinstance(r, PassiveLivenessCheckResponse), + ) diff --git a/tests/generated/test_phone.py b/tests/generated/test_phone.py new file mode 100644 index 0000000..61f4876 --- /dev/null +++ b/tests/generated/test_phone.py @@ -0,0 +1,183 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``phone`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.phone_risk_response import PhoneRiskResponse +from didit.types.components.phone_verification_check_response import PhoneVerificationCheckResponse +from didit.types.components.phone_verification_send_response import PhoneVerificationSendResponse +from tests.harness import Expect, check_operation + + +def test_phone_assess_risk() -> None: + """``POST /v3/phone/risk/``""" + check_operation( + Expect( + method="POST", + path="/v3/phone/risk/", + api="verification", + auth="api_key", + json_body={"phone_number": "+14155552671"}, + status=200, + response_json={ + "request_id": "4fd09d2e-9bc9-4c4e-9c46-7f625ac0ab62", + "status": "Approved", + "phone": { + "status": "Approved", + "phone_number_prefix": "+1", + "phone_number": "4155552671", + "full_number": "+14155552671", + "country_code": "US", + "country_name": "United States", + "carrier": {"name": "Example Mobile", "type": "mobile"}, + "is_disposable": False, + "is_virtual": False, + "verification_method": None, + "verification_attempts": 0, + "verified_at": None, + "warnings": [], + "lifecycle": [], + "matches": [], + "phone_intelligence": { + "score": 3.0, + "phone_type_risk": "Low", + "phone_trust_index": 97, + "network_stability_index": 95, + "is_disposable": False, + "is_ported": False, + "is_recently_ported": False, + "carrier_type": "mobile", + "reason_codes": [], + }, + "enrichment": { + "score": 3.0, + "phone_type_risk": "Low", + "phone_trust_index": 97, + "network_stability_index": 95, + "is_disposable": False, + "is_ported": False, + "is_recently_ported": False, + "carrier_type": "mobile", + "reason_codes": [], + }, + }, + "vendor_data": "lead-123", + "metadata": {"source": "signup_prescreen"}, + "created_at": "2026-09-01T10:30:00.123456+00:00", + }, + ), + lambda c: c.phone.assess_risk(phone_number="+14155552671"), + lambda c: c.phone.assess_risk(phone_number="+14155552671"), + lambda r: isinstance(r, PhoneRiskResponse), + lambda r: isinstance(r, PhoneRiskResponse), + ) + + +def test_phone_check_code() -> None: + """``POST /v3/phone/check/``""" + check_operation( + Expect( + method="POST", + path="/v3/phone/check/", + api="verification", + auth="api_key", + json_body={"phone_number": "+14155552671", "code": "123456"}, + status=200, + response_json={ + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Approved", + "message": "The verification code is correct.", + "phone": { + "status": "Approved", + "phone_number_prefix": "+1", + "phone_number": "4155552671", + "full_number": "+14155552671", + "country_code": "US", + "country_name": "United States", + "carrier": {"name": "AT&T", "type": "mobile"}, + "is_disposable": False, + "is_virtual": False, + "verification_method": "whatsapp", + "verification_attempts": 1, + "verified_at": "2026-06-12T01:24:47.311323Z", + "warnings": [], + "lifecycle": [ + { + "type": "PHONE_VERIFICATION_MESSAGE_SENT", + "timestamp": "2026-06-12T01:23:39.580554+00:00", + "details": { + "status": "Success", + "reason": None, + "channel": "whatsapp", + "actual_channel": "whatsapp", + }, + "fee": 0.0709, + }, + { + "type": "PHONE_DELIVERY_DELIVERED", + "timestamp": "2026-06-12T01:23:43.118220+00:00", + "details": {"channel": "whatsapp", "status": "delivered"}, + "fee": 0, + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2026-06-12T01:24:47.311201+00:00", + "details": {"code_tried": "123456", "status": "Approved"}, + "fee": 0, + }, + { + "type": "PHONE_VERIFICATION_APPROVED", + "timestamp": "2026-06-12T01:24:47.384292+00:00", + "details": None, + "fee": 0, + }, + ], + "matches": [], + }, + "vendor_data": "user-1234", + "metadata": None, + "created_at": "2026-06-12T01:24:47.401719+00:00", + }, + ), + lambda c: c.phone.check_code(phone_number="+14155552671", code="123456"), + lambda c: c.phone.check_code(phone_number="+14155552671", code="123456"), + lambda r: isinstance(r, PhoneVerificationCheckResponse), + lambda r: isinstance(r, PhoneVerificationCheckResponse), + ) + + +def test_phone_send_code() -> None: + """``POST /v3/phone/send/``""" + check_operation( + Expect( + method="POST", + path="/v3/phone/send/", + api="verification", + auth="api_key", + json_body={ + "phone_number": "+14155552671", + "options": {"preferred_channel": "whatsapp", "locale": "en-US"}, + "vendor_data": "user-1234", + }, + status=200, + response_json={ + "request_id": "e39cb057-92fc-4b59-b84e-02fec29a0f24", + "status": "Success", + "reason": None, + "vendor_data": "user-1234", + "metadata": None, + }, + ), + lambda c: c.phone.send_code( + phone_number="+14155552671", + options={"preferred_channel": "whatsapp", "locale": "en-US"}, + vendor_data="user-1234", + ), + lambda c: c.phone.send_code( + phone_number="+14155552671", + options={"preferred_channel": "whatsapp", "locale": "en-US"}, + vendor_data="user-1234", + ), + lambda r: isinstance(r, PhoneVerificationSendResponse), + lambda r: isinstance(r, PhoneVerificationSendResponse), + ) diff --git a/tests/generated/test_proof_of_address.py b/tests/generated/test_proof_of_address.py new file mode 100644 index 0000000..78db6b6 --- /dev/null +++ b/tests/generated/test_proof_of_address.py @@ -0,0 +1,123 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``proof_of_address`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.proof_of_address.verify_response import ProofOfAddressVerifyResponse +from tests.harness import Expect, check_operation + + +def test_proof_of_address_verify() -> None: + """``POST /v3/poa/``""" + check_operation( + Expect( + method="POST", + path="/v3/poa/", + api="verification", + auth="api_key", + multipart_fields=[ + "expected_address", + "expected_country", + "expected_first_name", + "expected_last_name", + "poa_languages_allowed", + "save_api_request", + "vendor_data", + ], + multipart_files=["document"], + status=200, + response_json={ + "request_id": "4b523a8d-ed32-45e4-8aa8-a89eda459d88", + "poa": { + "issuing_state": "ARG", + "document_type": "UTILITY_BILL", + "document_subtype": "WATER_BILL", + "document_language": "es", + "issuer": "Aguas del Norte", + "issue_date": "2026-04-10", + "expiration_date": None, + "poa_address": "AVDA. MONSEÑOR TAVELLA N° 1291, B° VILLA LAVALLE CP: 4400, CAPITAL - SALTA", + "poa_formatted_address": "Av. Monseñor Tavella 1291 b, A4400 Salta, Argentina", + "poa_parsed_address": { + "street_1": "Avenida Monseñor Tavella 1291", + "street_2": "b", + "city": "Salta", + "region": "Salta", + "country": "AR", + "postal_code": "A4400", + "document_location": {"latitude": -24.8208682, "longitude": -65.4130954}, + }, + "expected_details_address": None, + "expected_details_formatted_address": None, + "expected_details_parsed_address": None, + "name_on_document": "OESTE EMBOTELLADORA S.A.", + "extra_fields": { + "bank_account_number": None, + "bank_iban": None, + "bank_sort_code": None, + "bank_routing_number": None, + "bank_swift_bic": None, + "bank_branch_name": None, + "bank_branch_address": None, + "document_phone_number": None, + "additional_names": ["LA EMBOTELLADORA DEL NORTE SA"], + }, + "detected_codes": [ + { + "type": "QR_CODE", + "page_number": 1, + "position": [[820, 1120], [1002, 1120], [1002, 1302], [820, 1302]], + "x": 820, + "y": 1120, + "width": 182, + "height": 182, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "aHR0cHM6Ly9hZ3Vhc2RlbG5vcnRlLmNvbS9mYWN0dXJhLzk5ODgxMQ==", + "parsed_payload": "https://aguasdelnorte.com/factura/998811", + }, + { + "type": "CODE_128", + "page_number": 1, + "position": [[112, 1480], [560, 1480], [560, 1540], [112, 1540]], + "x": 112, + "y": 1480, + "width": 448, + "height": 60, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "MDA5OTg4MTEwMDA0Mg==", + "parsed_payload": "0099881100042", + }, + ], + "status": "Approved", + "warnings": [], + }, + "vendor_data": "user-123", + "metadata": None, + "created_at": "2026-06-12T02:29:02.439655+00:00", + }, + ), + lambda c: c.proof_of_address.verify( + document=b"file-content", + expected_address="Av. Monseñor Tavella 1291, Salta, Argentina", + expected_country="ARG", + expected_first_name="Sophia", + expected_last_name="Martinez", + poa_languages_allowed="en,es", + save_api_request=True, + vendor_data="user-123", + ), + lambda c: c.proof_of_address.verify( + document=b"file-content", + expected_address="Av. Monseñor Tavella 1291, Salta, Argentina", + expected_country="ARG", + expected_first_name="Sophia", + expected_last_name="Martinez", + poa_languages_allowed="en,es", + save_api_request=True, + vendor_data="user-123", + ), + lambda r: isinstance(r, ProofOfAddressVerifyResponse), + lambda r: isinstance(r, ProofOfAddressVerifyResponse), + ) diff --git a/tests/generated/test_questionnaires.py b/tests/generated/test_questionnaires.py new file mode 100644 index 0000000..283c8a2 --- /dev/null +++ b/tests/generated/test_questionnaires.py @@ -0,0 +1,316 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``questionnaires`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.questionnaire_detail import QuestionnaireDetail +from didit.types.components.questionnaire_list_item import QuestionnaireListItem +from tests.harness import Expect, check_operation + + +def test_questionnaires_create() -> None: + """``POST /v3/questionnaires/``""" + check_operation( + Expect( + method="POST", + path="/v3/questionnaires/", + api="verification", + auth="api_key", + json_body={ + "title": "Customer Onboarding", + "languages": ["en"], + "form_elements": [ + { + "id": "occupation", + "element_type": "short_text", + "label": {"en": "What is your occupation?"}, + "is_required": True, + "placeholder": {"en": "Software engineer"}, + }, + { + "id": "source_of_funds", + "element_type": "multiple_choice", + "label": {"en": "Source of funds"}, + "is_required": True, + "options": [ + {"value": "employment", "label": {"en": "Employment"}}, + {"value": "business", "label": {"en": "Business"}}, + {"value": "investments", "label": {"en": "Investments"}}, + {"value": "other", "label": {"en": "Other"}}, + ], + }, + ], + "description": "Additional information needed for verification", + "default_language": "en", + "is_active": True, + }, + status=201, + response_json={ + "questionnaire_id": "11111111-2222-3333-4444-555555555555", + "title": "Customer Onboarding", + "description": "Additional information needed for verification", + "languages": ["en"], + "default_language": "en", + "is_active": True, + "is_simple_questionnaire": True, + "graph": { + "start_node": "occupation", + "nodes": { + "occupation": { + "element_type": "SHORT_TEXT", + "is_required": True, + "title": {"en": "What is your occupation?"}, + "next": "source_of_funds", + }, + "source_of_funds": { + "element_type": "MULTIPLE_CHOICE", + "is_required": True, + "title": {"en": "Source of funds"}, + "choices": [ + {"value": "employment", "label": {"en": "Employment"}}, + {"value": "business", "label": {"en": "Business"}}, + ], + "next": None, + }, + }, + }, + "sections": [], + "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", + "version": 1, + "status": "published", + "published_at": "2026-04-23T10:00:00Z", + "is_editable": False, + }, + ), + lambda c: c.questionnaires.create( + title="Customer Onboarding", + languages=["en"], + form_elements=[ + { + "id": "occupation", + "element_type": "short_text", + "label": {"en": "What is your occupation?"}, + "is_required": True, + "placeholder": {"en": "Software engineer"}, + }, + { + "id": "source_of_funds", + "element_type": "multiple_choice", + "label": {"en": "Source of funds"}, + "is_required": True, + "options": [ + {"value": "employment", "label": {"en": "Employment"}}, + {"value": "business", "label": {"en": "Business"}}, + {"value": "investments", "label": {"en": "Investments"}}, + {"value": "other", "label": {"en": "Other"}}, + ], + }, + ], + description="Additional information needed for verification", + default_language="en", + is_active=True, + ), + lambda c: c.questionnaires.create( + title="Customer Onboarding", + languages=["en"], + form_elements=[ + { + "id": "occupation", + "element_type": "short_text", + "label": {"en": "What is your occupation?"}, + "is_required": True, + "placeholder": {"en": "Software engineer"}, + }, + { + "id": "source_of_funds", + "element_type": "multiple_choice", + "label": {"en": "Source of funds"}, + "is_required": True, + "options": [ + {"value": "employment", "label": {"en": "Employment"}}, + {"value": "business", "label": {"en": "Business"}}, + {"value": "investments", "label": {"en": "Investments"}}, + {"value": "other", "label": {"en": "Other"}}, + ], + }, + ], + description="Additional information needed for verification", + default_language="en", + is_active=True, + ), + lambda r: isinstance(r, QuestionnaireDetail), + lambda r: isinstance(r, QuestionnaireDetail), + ) + + +def test_questionnaires_delete() -> None: + """``DELETE /v3/questionnaires/{questionnaire_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/questionnaires/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.questionnaires.delete("00000000-0000-4000-8000-000000000001"), + lambda c: c.questionnaires.delete("00000000-0000-4000-8000-000000000001"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_questionnaires_list() -> None: + """``GET /v3/questionnaires/``""" + check_operation( + Expect( + method="GET", + path="/v3/questionnaires/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "title": "KYC Questionnaire", + "description": "Additional verification questions", + "languages": ["en", "es"], + "default_language": "en", + "is_active": True, + "is_simple_questionnaire": True, + "created_at": "2026-04-23T09:55:00Z", + "updated_at": "2026-04-23T10:00:00Z", + "question_types": ["SHORT_TEXT", "MULTIPLE_CHOICE", "FILE_UPLOAD"], + "workflow_names": [], + "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", + "version": 1, + "status": "published", + "published_at": "2026-04-23T10:00:00Z", + "is_editable": False, + } + ], + }, + ), + lambda c: c.questionnaires.list(limit=1, offset=1), + lambda c: c.questionnaires.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, QuestionnaireListItem) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, QuestionnaireListItem) for x1 in r.results) + ), + ) + + +def test_questionnaires_retrieve() -> None: + """``GET /v3/questionnaires/{questionnaire_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/questionnaires/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "questionnaire_id": "11111111-2222-3333-4444-555555555555", + "title": "Customer Onboarding", + "description": "Additional information needed for verification", + "languages": ["en"], + "default_language": "en", + "is_active": True, + "is_simple_questionnaire": True, + "graph": { + "start_node": "occupation", + "nodes": { + "occupation": { + "element_type": "SHORT_TEXT", + "is_required": True, + "title": {"en": "What is your occupation?"}, + "next": "source_of_funds", + }, + "source_of_funds": { + "element_type": "MULTIPLE_CHOICE", + "is_required": True, + "title": {"en": "Source of funds"}, + "choices": [ + {"value": "employment", "label": {"en": "Employment"}}, + {"value": "business", "label": {"en": "Business"}}, + ], + "next": None, + }, + }, + }, + "sections": [ + { + "title": {}, + "description": {}, + "items": [ + { + "uuid": "11111111-2222-3333-4444-666666666666", + "value": "occupation", + "element_type": "SHORT_TEXT", + "is_required": True, + "title": {"en": "What is your occupation?"}, + } + ], + } + ], + "questionnaire_group_id": "11111111-2222-3333-4444-555555555555", + "version": 1, + "status": "published", + "published_at": "2026-04-23T10:00:00Z", + "is_editable": False, + }, + ), + lambda c: c.questionnaires.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.questionnaires.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, QuestionnaireDetail), + lambda r: isinstance(r, QuestionnaireDetail), + ) + + +def test_questionnaires_update() -> None: + """``PATCH /v3/questionnaires/{questionnaire_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/questionnaires/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"title": "Updated Customer Onboarding"}, + status=200, + response_json={ + "questionnaire_id": "00000000-0000-4000-8000-000000000003", + "title": "sample", + "description": "sample", + "languages": ["sample"], + "default_language": "sample", + "is_active": True, + "is_simple_questionnaire": True, + "graph": {}, + "sections": [{}], + "questionnaire_group_id": "00000000-0000-4000-8000-000000000003", + "version": 1, + "status": "draft", + "published_at": "2026-01-01T00:00:00Z", + "is_editable": True, + }, + strict_type=QuestionnaireDetail, + ), + lambda c: c.questionnaires.update( + "00000000-0000-4000-8000-000000000001", title="Updated Customer Onboarding" + ), + lambda c: c.questionnaires.update( + "00000000-0000-4000-8000-000000000001", title="Updated Customer Onboarding" + ), + lambda r: isinstance(r, QuestionnaireDetail), + lambda r: isinstance(r, QuestionnaireDetail), + ) diff --git a/tests/generated/test_regulatory_reports.py b/tests/generated/test_regulatory_reports.py new file mode 100644 index 0000000..0ef2c60 --- /dev/null +++ b/tests/generated/test_regulatory_reports.py @@ -0,0 +1,422 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``regulatory_reports`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.regulatory_report_detail import RegulatoryReportDetail +from didit.types.regulatory_reports.retrieve_download_url_response import ( + RegulatoryReportsRetrieveDownloadUrlResponse, +) +from didit.types.regulatory_reports.validate_response import RegulatoryReportsValidateResponse +from tests.harness import Expect, check_operation + + +def test_regulatory_reports_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/", + api="verification", + auth="api_key", + json_body={ + "case": "00000000-0000-4000-8000-000000000001", + "template": "00000000-0000-4000-8000-000000000001", + }, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000004"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RegulatoryReportDetail, + ), + lambda c: c.regulatory_reports.create( + case="00000000-0000-4000-8000-000000000001", + template="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.regulatory_reports.create( + case="00000000-0000-4000-8000-000000000001", + template="00000000-0000-4000-8000-000000000001", + ), + lambda r: isinstance(r, RegulatoryReportDetail), + lambda r: isinstance(r, RegulatoryReportDetail), + ) + + +def test_regulatory_reports_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/5ec00000-0000-4000-8000-000000000007/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.regulatory_reports.delete("5ec00000-0000-4000-8000-000000000007"), + lambda c: c.regulatory_reports.delete("5ec00000-0000-4000-8000-000000000007"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_regulatory_reports_finalize() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/finalize/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/5ec00000-0000-4000-8000-000000000007/finalize/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000004"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RegulatoryReportDetail, + ), + lambda c: c.regulatory_reports.finalize("5ec00000-0000-4000-8000-000000000007"), + lambda c: c.regulatory_reports.finalize("5ec00000-0000-4000-8000-000000000007"), + lambda r: isinstance(r, RegulatoryReportDetail), + lambda r: isinstance(r, RegulatoryReportDetail), + ) + + +def test_regulatory_reports_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/", + api="verification", + auth="api_key", + query={ + "limit": ["1"], + "offset": ["1"], + "status": ["DRAFT"], + "report_type": ["SAR"], + "case": ["00000000-0000-4000-8000-000000000001"], + }, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "case_uuid": "00000000-0000-4000-8000-000000000005", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000006"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.regulatory_reports.list( + limit=1, + offset=1, + status="DRAFT", + report_type="SAR", + case="00000000-0000-4000-8000-000000000001", + ), + lambda c: c.regulatory_reports.list( + limit=1, + offset=1, + status="DRAFT", + report_type="SAR", + case="00000000-0000-4000-8000-000000000001", + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, RegulatoryReportDetail) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, RegulatoryReportDetail) for x1 in r.results) + ), + ) + + +def test_regulatory_reports_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/5ec00000-0000-4000-8000-000000000007/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000004"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RegulatoryReportDetail, + ), + lambda c: c.regulatory_reports.retrieve("5ec00000-0000-4000-8000-000000000007"), + lambda c: c.regulatory_reports.retrieve("5ec00000-0000-4000-8000-000000000007"), + lambda r: isinstance(r, RegulatoryReportDetail), + lambda r: isinstance(r, RegulatoryReportDetail), + ) + + +def test_regulatory_reports_retrieve_download_url() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/download-url/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/5ec00000-0000-4000-8000-000000000007/download-url/", + api="verification", + auth="api_key", + query={"file": ["pdf"]}, + status=200, + response_json={"download_url": "https://example.com/resource", "file": "pdf"}, + strict_type=RegulatoryReportsRetrieveDownloadUrlResponse, + ), + lambda c: c.regulatory_reports.retrieve_download_url( + "5ec00000-0000-4000-8000-000000000007", file="pdf" + ), + lambda c: c.regulatory_reports.retrieve_download_url( + "5ec00000-0000-4000-8000-000000000007", file="pdf" + ), + lambda r: isinstance(r, RegulatoryReportsRetrieveDownloadUrlResponse), + lambda r: isinstance(r, RegulatoryReportsRetrieveDownloadUrlResponse), + ) + + +def test_regulatory_reports_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/5ec00000-0000-4000-8000-000000000007/", + api="verification", + auth="api_key", + json_body={"jurisdiction": "sa"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "case_uuid": "00000000-0000-4000-8000-000000000003", + "case_number": "sample", + "report_type": "SAR", + "report_type_display": "sample", + "report_code": "sample", + "report_number": "sample", + "status": "DRAFT", + "status_display": "sample", + "jurisdiction": "sa", + "rentity_id": "sample", + "rentity_branch": "sample", + "submission_code": "E", + "entity_reference": "sample", + "fiu_ref_number": "sample", + "currency_code_local": "sam", + "submission_date": "2026-01-01T00:00:00Z", + "reporting_person": {}, + "location": {}, + "reason": "sample", + "action": "sample", + "activity": "sample", + "narrative": { + "who": "sample", + "what": "sample", + "when": "sample", + "where": "sample", + "why": "sample", + "how": "sample", + }, + "report_indicators": ["sample"], + "included_fields": ["sample"], + "transactions": ["00000000-0000-4000-8000-000000000004"], + "validation_errors": [{"field": "sample", "message": "sample"}], + "validated_at": "2026-01-01T00:00:00Z", + "finalized_at": "2026-01-01T00:00:00Z", + "created_by_email": "sample", + "created_by_name": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=RegulatoryReportDetail, + ), + lambda c: c.regulatory_reports.update( + "5ec00000-0000-4000-8000-000000000007", jurisdiction="sa" + ), + lambda c: c.regulatory_reports.update( + "5ec00000-0000-4000-8000-000000000007", jurisdiction="sa" + ), + lambda r: isinstance(r, RegulatoryReportDetail), + lambda r: isinstance(r, RegulatoryReportDetail), + ) + + +def test_regulatory_reports_validate() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/regulatory-reports/{report_uuid}/validate/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/regulatory-reports/5ec00000-0000-4000-8000-000000000007/validate/", + api="verification", + auth="api_key", + status=200, + response_json={ + "status": "DRAFT", + "is_valid": True, + "errors": [{"field": "sample", "message": "sample"}], + }, + strict_type=RegulatoryReportsValidateResponse, + ), + lambda c: c.regulatory_reports.validate("5ec00000-0000-4000-8000-000000000007"), + lambda c: c.regulatory_reports.validate("5ec00000-0000-4000-8000-000000000007"), + lambda r: isinstance(r, RegulatoryReportsValidateResponse), + lambda r: isinstance(r, RegulatoryReportsValidateResponse), + ) diff --git a/tests/generated/test_report_templates.py b/tests/generated/test_report_templates.py new file mode 100644 index 0000000..11bf549 --- /dev/null +++ b/tests/generated/test_report_templates.py @@ -0,0 +1,152 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``report_templates`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.report_template_detail import ReportTemplateDetail +from tests.harness import Expect, check_operation + + +def test_report_templates_create() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/report-templates/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/report-templates/", + api="verification", + auth="api_key", + json_body={"name": "sample", "jurisdiction": "sa", "report_type": "SAR"}, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "jurisdiction": "sa", + "report_type": "SAR", + "report_type_display": "sample", + "field_config": {}, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=ReportTemplateDetail, + ), + lambda c: c.report_templates.create(name="sample", jurisdiction="sa", report_type="SAR"), + lambda c: c.report_templates.create(name="sample", jurisdiction="sa", report_type="SAR"), + lambda r: isinstance(r, ReportTemplateDetail), + lambda r: isinstance(r, ReportTemplateDetail), + ) + + +def test_report_templates_delete() -> None: + """``DELETE /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/report-templates/7ec00000-0000-4000-8000-000000000006/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.report_templates.delete("7ec00000-0000-4000-8000-000000000006"), + lambda c: c.report_templates.delete("7ec00000-0000-4000-8000-000000000006"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_report_templates_list() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/report-templates/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/report-templates/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "name": "sample", + "jurisdiction": "sa", + "report_type": "SAR", + "report_type_display": "sample", + "field_config": {}, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.report_templates.list(limit=1, offset=1), + lambda c: c.report_templates.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, ReportTemplateDetail) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, ReportTemplateDetail) for x1 in r.results) + ), + ) + + +def test_report_templates_retrieve() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/report-templates/7ec00000-0000-4000-8000-000000000006/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "jurisdiction": "sa", + "report_type": "SAR", + "report_type_display": "sample", + "field_config": {}, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=ReportTemplateDetail, + ), + lambda c: c.report_templates.retrieve("7ec00000-0000-4000-8000-000000000006"), + lambda c: c.report_templates.retrieve("7ec00000-0000-4000-8000-000000000006"), + lambda r: isinstance(r, ReportTemplateDetail), + lambda r: isinstance(r, ReportTemplateDetail), + ) + + +def test_report_templates_update() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/report-templates/{template_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/report-templates/7ec00000-0000-4000-8000-000000000006/", + api="verification", + auth="api_key", + json_body={"name": "sample"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "name": "sample", + "jurisdiction": "sa", + "report_type": "SAR", + "report_type_display": "sample", + "field_config": {}, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=ReportTemplateDetail, + ), + lambda c: c.report_templates.update("7ec00000-0000-4000-8000-000000000006", name="sample"), + lambda c: c.report_templates.update("7ec00000-0000-4000-8000-000000000006", name="sample"), + lambda r: isinstance(r, ReportTemplateDetail), + lambda r: isinstance(r, ReportTemplateDetail), + ) diff --git a/tests/generated/test_sessions.py b/tests/generated/test_sessions.py new file mode 100644 index 0000000..e2d6dc7 --- /dev/null +++ b/tests/generated/test_sessions.py @@ -0,0 +1,2849 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``sessions`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.review_item import ReviewItem +from didit.types.components.session_import_error import SessionImportError +from didit.types.components.session_import_job import SessionImportJob +from didit.types.sessions.batch_delete_response import ( + SessionsBatchDeleteResponse, + SessionsBatchDeleteResponse202, +) +from didit.types.sessions.create_response import SessionsCreateResponse +from didit.types.sessions.delete_response import SessionsDeleteResponse +from didit.types.sessions.list_networks_response import SessionsListNetworksResponse +from didit.types.sessions.list_response import ( + SessionsListResponseResultsKybSessionRowSessionKindBusiness, + SessionsListResponseResultsKycSessionRowSessionKindUser, +) +from didit.types.sessions.preview_resubmission_response import SessionsPreviewResubmissionResponse +from didit.types.sessions.retrieve_decision_response import SessionsRetrieveDecisionResponse +from didit.types.sessions.retrieve_deletion_job_response import SessionsRetrieveDeletionJobResponse +from didit.types.sessions.reviews.create_response import SessionsReviewsCreateResponse +from didit.types.sessions.share_response import SessionsShareResponse +from didit.types.sessions.update_data_response import SessionsUpdateDataResponse +from didit.types.sessions.update_feature_status_response import SessionsUpdateFeatureStatusResponse +from didit.types.sessions.update_kyb_company_data_response import ( + SessionsUpdateKybCompanyDataResponse, +) +from didit.types.sessions.update_kyb_party_response import SessionsUpdateKybPartyResponse +from didit.types.sessions.update_poa_data_response import SessionsUpdatePoaDataResponse +from didit.types.sessions.update_status_response import SessionsUpdateStatusResponse +from tests.harness import BINARY_BODY, Expect, check_operation + + +def test_sessions_batch_delete() -> None: + """``POST /v3/sessions/delete/``""" + check_operation( + Expect( + method="POST", + path="/v3/sessions/delete/", + api="verification", + auth="api_key", + json_body={"session_numbers": ["1001", "1002", "1003"]}, + status=200, + response_json={ + "results": [ + { + "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", + "session_number": 1001, + "face_retention_outcome": "deleted", + "biometric_template_uuid": None, + "biometric_template_expires_at": None, + "deleted_child_verifications": 0, + }, + { + "session_id": "b8e1d2c3-4f5a-4b6c-8d7e-9f0a1b2c3d4e", + "session_number": 1002, + "face_retention_outcome": "retained_with_user", + "biometric_template_uuid": "9d2f4b6e-1c3a-4e8f-b7d5-0a1c2e3f4a5b", + "biometric_template_expires_at": "2027-10-09T10:15:00Z", + "deleted_child_verifications": 0, + }, + { + "session_id": "7a1e9c2d-5f4b-4c3a-8e2d-1b0a9f8e7d6c", + "session_number": 1003, + "face_retention_outcome": "ineligible_no_vendor_user", + "biometric_template_uuid": None, + "biometric_template_expires_at": None, + "deleted_child_verifications": 0, + }, + { + "session_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f", + "session_number": 1004, + "face_retention_outcome": "failed_retryable", + "biometric_template_uuid": None, + "biometric_template_expires_at": None, + "deleted_child_verifications": 0, + }, + ] + }, + ), + lambda c: c.sessions.batch_delete(session_numbers=["1001", "1002", "1003"]), + lambda c: c.sessions.batch_delete(session_numbers=["1001", "1002", "1003"]), + lambda r: ( + (isinstance(r, SessionsBatchDeleteResponse)) + or (isinstance(r, SessionsBatchDeleteResponse202)) + ), + lambda r: ( + (isinstance(r, SessionsBatchDeleteResponse)) + or (isinstance(r, SessionsBatchDeleteResponse202)) + ), + ) + + +def test_sessions_bulk_update_aml_hit_status() -> None: + """``PATCH /v3/session/{sessionId}/bulk-update-aml-hit-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/bulk-update-aml-hit-status/", + api="verification", + auth="api_key", + query={"node_id": ["feature_aml_1"]}, + json_body={"hit_updates": [{"hit_id": "abc123", "review_status": "Unreviewed"}]}, + status=200, + response_json={ + "message": "Updated 2 hit(s) successfully", + "updated_hits": ["abc123", "def456"], + }, + ), + lambda c: c.sessions.bulk_update_aml_hit_status( + "11111111-2222-3333-4444-555555555555", + hit_updates=[{"hit_id": "abc123", "review_status": "Unreviewed"}], + node_id_query="feature_aml_1", + ), + lambda c: c.sessions.bulk_update_aml_hit_status( + "11111111-2222-3333-4444-555555555555", + hit_updates=[{"hit_id": "abc123", "review_status": "Unreviewed"}], + node_id_query="feature_aml_1", + ), + lambda r: True, + lambda r: True, + ) + + +def test_sessions_create() -> None: + """``POST /v3/session/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/", + api="verification", + auth="api_key", + json_body={ + "workflow_id": "11111111-2222-3333-4444-555555555555", + "vendor_data": "user-123", + "callback": "https://example.com/verification/callback", + "callback_method": "both", + "metadata": {"user_type": "premium", "account_id": "ABC123"}, + "language": "en", + "contact_details": { + "email": "john.doe@example.com", + "send_notification_emails": True, + "email_lang": "en", + "phone": "+14155552671", + }, + "expected_details": { + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1990-05-15", + "nationality": "USA", + "id_country": "USA", + "expected_document_types": ["P", "ID"], + }, + }, + status=201, + response_json={ + "session_id": "11111111-2222-3333-4444-555555555555", + "session_number": 43762, + "session_token": "sample-token", + "url": "https://verify.didit.me/en/session/3FaJ9wLqX2Mz", + "vendor_data": "user-123", + "metadata": {"user_type": "premium", "account_id": "ABC123"}, + "status": "Not Started", + "workflow_id": "11111111-2222-3333-4444-555555555555", + "workflow_version": 3, + "callback": "https://example.com/verification/callback", + }, + ), + lambda c: c.sessions.create( + workflow_id="11111111-2222-3333-4444-555555555555", + vendor_data="user-123", + callback="https://example.com/verification/callback", + callback_method="both", + metadata={"user_type": "premium", "account_id": "ABC123"}, + language="en", + contact_details={ + "email": "john.doe@example.com", + "send_notification_emails": True, + "email_lang": "en", + "phone": "+14155552671", + }, + expected_details={ + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1990-05-15", + "nationality": "USA", + "id_country": "USA", + "expected_document_types": ["P", "ID"], + }, + ), + lambda c: c.sessions.create( + workflow_id="11111111-2222-3333-4444-555555555555", + vendor_data="user-123", + callback="https://example.com/verification/callback", + callback_method="both", + metadata={"user_type": "premium", "account_id": "ABC123"}, + language="en", + contact_details={ + "email": "john.doe@example.com", + "send_notification_emails": True, + "email_lang": "en", + "phone": "+14155552671", + }, + expected_details={ + "first_name": "John", + "last_name": "Doe", + "date_of_birth": "1990-05-15", + "nationality": "USA", + "id_country": "USA", + "expected_document_types": ["P", "ID"], + }, + ), + lambda r: isinstance(r, SessionsCreateResponse), + lambda r: isinstance(r, SessionsCreateResponse), + ) + + +def test_sessions_delete() -> None: + """``DELETE /v3/session/{sessionId}/delete/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/session/11111111-2222-3333-4444-555555555555/delete/", + api="verification", + auth="api_key", + json_body={ + "retain_face_embeddings": True, + "face_retention_days": 365, + "instruction_id": "post-approval-cleanup-2026-08", + }, + status=200, + response_json={ + "session_id": "4c5c7f3a-3b1d-4b7e-9c39-2b4f7e6a1d02", + "session_number": 1024, + "face_retention_outcome": "deleted", + "biometric_template_uuid": None, + "biometric_template_expires_at": None, + "deleted_child_verifications": 0, + }, + ), + lambda c: c.sessions.delete( + "11111111-2222-3333-4444-555555555555", + retain_face_embeddings=True, + face_retention_days=365, + instruction_id="post-approval-cleanup-2026-08", + ), + lambda c: c.sessions.delete( + "11111111-2222-3333-4444-555555555555", + retain_face_embeddings=True, + face_retention_days=365, + instruction_id="post-approval-cleanup-2026-08", + ), + lambda r: isinstance(r, SessionsDeleteResponse), + lambda r: isinstance(r, SessionsDeleteResponse), + ) + + +def test_sessions_generate_pdf() -> None: + """``GET /v3/session/{sessionId}/generate-pdf/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/generate-pdf/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.sessions.generate_pdf("11111111-2222-3333-4444-555555555555"), + lambda c: c.sessions.generate_pdf("11111111-2222-3333-4444-555555555555"), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_sessions_import_shared() -> None: + """``POST /v3/session/import-shared/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/import-shared/", + api="verification", + auth="api_key", + json_body={ + "share_token": "sample-token", + "trust_review": False, + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "vendor_data": "user-1", + }, + status=201, + response_json={ + "session_id": "11111111-2222-3333-4444-555555555555", + "session_kind": "user", + "shared_from_session": "99999999-8888-7777-6666-555555555555", + "session_number": 43762, + "session_url": None, + "status": "In Review", + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "vendor_data": "user-1", + "created_at": "2026-05-17T08:42:11Z", + "expires_at": "2026-05-24T08:42:11Z", + }, + ), + lambda c: c.sessions.import_shared( + share_token="sample-token", + trust_review=False, + workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", + vendor_data="user-1", + ), + lambda c: c.sessions.import_shared( + share_token="sample-token", + trust_review=False, + workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", + vendor_data="user-1", + ), + lambda r: True, + lambda r: True, + ) + + +def test_sessions_list() -> None: + """``GET /v3/sessions/``""" + check_operation( + Expect( + method="GET", + path="/v3/sessions/", + api="verification", + auth="api_key", + query={ + "session_kind": ["all"], + "limit": ["20"], + "offset": ["20"], + "status": ["Approved,In Review"], + "vendor_data": ["your-user-id-123"], + "search": ["jane doe"], + "workflow_id": ["9f9b1234-aaaa-bbbb-cccc-1234567890ab"], + "date_from": ["2026-06-01"], + "date_to": ["2026-06-30"], + "country": ["ESP,USA"], + "document_type": ["P,ID"], + "document_name": ["Spain - Id Card (2021)"], + "region": ["CA,TX"], + "didit_internal_id": ["d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147"], + "session_type": ["HOSTED"], + "api_service": ["AML"], + "screened_full_name": ["Jane Doe"], + "warning": ["POSSIBLE_FRAUD"], + "status_initialized_as_in_review": ["true"], + "is_kyc_to_be_reviewed": ["true"], + "euuid": ["0f8fad5b-d9cb-469f-a165-70867728950e"], + "organization_name": ["Acme Inc"], + }, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "session_id": "5b3720ed-d429-42ef-b67f-37ea805f48ee", + "session_kind": "user", + "session_number": 720, + "session_url": "https://verify.didit.me/session/CuXoBYCYVO5Y", + "portrait_image": "https:///portraits/5b3720ed.jpg?X-Amz-Expires=3600&X-Amz-Signature=...", + "document_type": "Passport", + "full_name": "Jane Marie Doe", + "country": "ESP", + "status": "Approved", + "vendor_data": "your-user-id-123", + "didit_internal_id": "d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", + "created_at": "2026-06-09T12:26:54.328364Z", + "features": [ + {"feature": "ID_VERIFICATION", "status": "Approved"}, + {"feature": "LIVENESS", "status": "Approved"}, + {"feature": "FACE_MATCH", "status": "Approved"}, + {"feature": "AML", "status": "Approved"}, + {"feature": "IP_ANALYSIS", "status": "Approved"}, + ], + "phone_number": {"number": "+14155552671", "is_verified": True}, + "email_address": {"email": "jane@example.com", "is_verified": True}, + "additional_details": { + "aml": { + "total_hits": 0, + "entity_type": "person", + "is_adverse_media_fetched": False, + }, + "liveness": {"method": "PASSIVE", "score": 0.98}, + "face_match": {"score": 0.95}, + }, + }, + { + "session_id": "038ad62f-5ebd-46c2-afe9-4b2956bf687f", + "session_kind": "user", + "session_number": 719, + "session_url": "https://verify.didit.me/session/7RGKiO6ioGH6", + "portrait_image": None, + "document_type": None, + "full_name": None, + "country": None, + "status": "Not Started", + "vendor_data": "your-user-id-124", + "didit_internal_id": "eb4e679d-ca4b-461f-9685-687bac29ebdb", + "created_at": "2026-06-09T08:02:23.343053Z", + "features": [ + {"feature": "ID_VERIFICATION", "status": "Not Finished"}, + {"feature": "LIVENESS", "status": "Not Finished"}, + {"feature": "FACE_MATCH", "status": "Not Finished"}, + {"feature": "AML", "status": "Not Finished"}, + {"feature": "IP_ANALYSIS", "status": "Not Finished"}, + ], + "phone_number": None, + "email_address": None, + "additional_details": None, + }, + ], + }, + ), + lambda c: c.sessions.list( + session_kind="all", + limit=20, + offset=20, + status="Approved,In Review", + vendor_data="your-user-id-123", + search="jane doe", + workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", + date_from="2026-06-01", + date_to="2026-06-30", + country="ESP,USA", + document_type="P,ID", + document_name="Spain - Id Card (2021)", + region="CA,TX", + didit_internal_id="d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", + session_type="HOSTED", + api_service="AML", + screened_full_name="Jane Doe", + warning="POSSIBLE_FRAUD", + status_initialized_as_in_review=True, + is_kyc_to_be_reviewed=True, + euuid="0f8fad5b-d9cb-469f-a165-70867728950e", + organization_name="Acme Inc", + ), + lambda c: c.sessions.list( + session_kind="all", + limit=20, + offset=20, + status="Approved,In Review", + vendor_data="your-user-id-123", + search="jane doe", + workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", + date_from="2026-06-01", + date_to="2026-06-30", + country="ESP,USA", + document_type="P,ID", + document_name="Spain - Id Card (2021)", + region="CA,TX", + didit_internal_id="d9e78474-6c8d-4f2a-bb90-6f5c2f3a1147", + session_type="HOSTED", + api_service="AML", + screened_full_name="Jane Doe", + warning="POSSIBLE_FRAUD", + status_initialized_as_in_review=True, + is_kyc_to_be_reviewed=True, + euuid="0f8fad5b-d9cb-469f-a165-70867728950e", + organization_name="Acme Inc", + ), + lambda r: ( + isinstance(r, SyncPage) + and all( + (isinstance(x1, SessionsListResponseResultsKycSessionRowSessionKindUser)) + or (isinstance(x1, SessionsListResponseResultsKybSessionRowSessionKindBusiness)) + for x1 in r.results + ) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all( + (isinstance(x1, SessionsListResponseResultsKycSessionRowSessionKindUser)) + or (isinstance(x1, SessionsListResponseResultsKybSessionRowSessionKindBusiness)) + for x1 in r.results + ) + ), + ) + + +def test_sessions_list_networks() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/sessions/{session_id}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/sessions/00000000-0000-4000-8000-000000000001/networks/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "network": { + "uuid": "00000000-0000-4000-8000-000000000007", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + }, + "subject": {}, + "signal_types": ["ip_address"], + "signal_count": 1, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=SessionsListNetworksResponse, + ), + lambda c: c.sessions.list_networks("00000000-0000-4000-8000-000000000001"), + lambda c: c.sessions.list_networks("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, SessionsListNetworksResponse), + lambda r: isinstance(r, SessionsListNetworksResponse), + ) + + +def test_sessions_preview_resubmission() -> None: + """``POST /v3/session/{sessionId}/resubmission-preview/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/11111111-2222-3333-4444-555555555555/resubmission-preview/", + api="verification", + auth="api_key", + json_body={"email_address": "user@example.com"}, + status=200, + response_json={ + "recipient": "user@example.com", + "sender": "sample", + "subject": "sample", + "html": "sample", + "nodes_to_resubmit": [{"node_id": "feature_ocr", "feature": "OCR"}], + "requires_user_action": True, + "preview_token": "sample-token", + }, + strict_type=SessionsPreviewResubmissionResponse, + ), + lambda c: c.sessions.preview_resubmission( + "11111111-2222-3333-4444-555555555555", email_address="user@example.com" + ), + lambda c: c.sessions.preview_resubmission( + "11111111-2222-3333-4444-555555555555", email_address="user@example.com" + ), + lambda r: isinstance(r, SessionsPreviewResubmissionResponse), + lambda r: isinstance(r, SessionsPreviewResubmissionResponse), + ) + + +def test_sessions_retrieve_decision() -> None: + """``GET /v3/session/{sessionId}/decision/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/11111111-2222-3333-4444-555555555555/decision/", + api="verification", + auth="api_key", + query={"include": ["events"]}, + status=200, + response_json={ + "session_id": "11111111-2222-3333-4444-555555555555", + "session_kind": "user", + "session_number": 43762, + "session_url": "https://verify.didit.me/session/11111111-2222-3333-4444-555555555555", + "status": "In Review", + "environment": "live", + "workflow_id": "11111111-2222-3333-4444-555555555555", + "features": [ + "ID_VERIFICATION", + "NFC", + "LIVENESS", + "FACE_MATCH", + "POA", + "PHONE", + "DATABASE_VALIDATION", + "AML", + "IP_ANALYSIS", + ], + "vendor_data": "11111111-1111-1111-1111-111111111111", + "metadata": {"user_type": "premium", "account_id": "ABC123"}, + "expected_details": {"first_name": "Carmen", "last_name": "Española Española"}, + "contact_details": { + "email": "carmen@example.com", + "email_lang": "es", + "send_notification_emails": False, + "phone": None, + }, + "callback": "https://verify.didit.me/", + "id_verifications": [ + { + "node_id": "feature_ocr_1", + "status": "Approved", + "document_type": "Identity Card", + "document_subtype": "ID_CARD_GENERIC", + "document_number": "CAA000000", + "personal_number": "99999999R", + "portrait_image": "https:///portrait.jpg", + "front_image": "https:///front.jpg", + "front_video": "https:///front.mp4", + "back_image": "https:///back.jpg", + "back_video": "https:///back.mp4", + "full_front_image": "https:///full_front.jpg", + "full_back_image": "https:///full_back.jpg", + "front_image_camera_front": "https:///front_camera_front.jpg", + "back_image_camera_front": "https:///back_camera_front.jpg", + "date_of_birth": "1980-01-01", + "age": 45, + "expiration_date": "2031-06-02", + "date_of_issue": "2021-06-02", + "issuing_state": "ESP", + "issuing_state_name": "Spain", + "first_name": "Carmen", + "last_name": "Española Española", + "full_name": "Carmen Española Española", + "gender": "F", + "address": "Avda de Madrid 34, Madrid, Madrid", + "formatted_address": "Avda de Madrid 34, Madrid, Madrid 28822, Spain", + "place_of_birth": "Madrid", + "marital_status": "Single", + "nationality": "ESP", + "extra_fields": {"dl_categories": [], "blood_group": None}, + "mrz": { + "surname": "ESPANOLA ESPANOLA", + "name": "CARMEN", + "country": "ESP", + "nationality": "ESP", + "birth_date": "800101", + "expiry_date": "310602", + "sex": "F", + "document_type": "ID", + "document_number": "CAA000000", + "optional_data": "99999999R", + "optional_data_2": "", + "birth_date_hash": "4", + "expiry_date_hash": "8", + "document_number_hash": "4", + "final_hash": "1", + "personal_number": "99999999R", + "warnings": [], + "errors": [], + "mrz_type": "TD1", + "mrz_string": "IDESPCAA000000499999999R<<<<<<\n8001014F3106028ESP<<<<<<<<<<<1\nESPANOLA/match_face.jpg", + "status": "Approved", + "is_blocklisted": False, + "is_allowlisted": False, + "api_service": None, + "source": "session", + } + ], + "front_image_quality_score": { + "focus_score": 85.3, + "brightness_score": 92.1, + "brightness_issue": "ok", + "is_document_fully_visible": True, + "resolution_score": 72.4, + "overall_score": 88.7, + }, + "back_image_quality_score": { + "focus_score": 79.1, + "brightness_score": 90.5, + "brightness_issue": "ok", + "is_document_fully_visible": True, + "resolution_score": 72.4, + "overall_score": 85.2, + }, + "extra_files": ["https:///extra_id_verification.jpg"], + "matches": [ + { + "session_id": "7d4f1f60-9c2e-4b8a-8f3d-2a1b5c6d7e8f", + "session_number": 1234, + "vendor_data": "user_ref_001", + "verification_date": "2024-06-15T10:30:00Z", + "user_details": { + "name": "John Smith", + "document_type": "ID", + "document_number": "CLM148418", + }, + "status": "Approved", + "is_blocklisted": False, + "api_service": None, + "front_image_url": "https:///document_front.jpg", + }, + { + "session_id": "b2c8e4d1-6a3f-4e7b-95c0-1d8f2a6b4c9e", + "session_number": 1235, + "vendor_data": None, + "verification_date": "2024-05-20T14:45:00Z", + "user_details": { + "name": "John Smith", + "document_type": "ID", + "document_number": "CLM148418", + }, + "status": "Declined", + "is_blocklisted": True, + "api_service": "ID_VERIFICATION", + "front_image_url": "https:///document_front_2.jpg", + }, + ], + "warnings": [ + { + "feature": "ID_VERIFICATION", + "risk": "QR_NOT_DETECTED", + "additional_data": None, + "log_type": "information", + "short_description": "QR not detected", + "long_description": "The system couldn't find or read the QR code on the document, which is necessary for document verification. This could be due to poor image quality or an unsupported document type.", + "node_id": "feature_ocr_1", + } + ], + "verification_method": "document", + "assurance": "documentary", + "wallet_provider": None, + "fallback_from": None, + "id_lookup": None, + "wallet_verification": None, + "document_liveness": { + "portrait_replace": { + "score": 96.46, + "front": 96.46, + "back": 98.2, + "decline_threshold": 35, + "review_threshold": 70, + "bucket": "approve", + }, + "printed_copy": { + "score": 91.2, + "front": 91.2, + "back": 93.7, + "decline_threshold": 35, + "review_threshold": 77, + "bucket": "approve", + }, + "screen_replay": { + "score": 99.9, + "front": 99.9, + "back": 99.5, + "decline_threshold": 30, + "review_threshold": 70, + "bucket": "approve", + }, + }, + }, + { + "node_id": "feature_ocr_2", + "status": "Approved", + "issuing_state": "ZAF", + "issuing_state_name": "South Africa", + "first_name": "Thabo", + "last_name": "Mokoena", + "full_name": "Thabo Mokoena", + "date_of_birth": "1991-06-15", + "personal_number": "9106155043088", + "verification_method": "id_lookup", + "assurance": "data_match", + "wallet_provider": None, + "fallback_from": None, + "id_lookup": { + "source": "Department of Home Affairs", + "checked_at": "2026-09-04T09:12:44Z", + "attempts": 1, + "max_attempts": 2, + "outcome": "match", + "comparison": [ + { + "field": "identification_number", + "label": "ID number", + "provided": "9106155043088", + "record": "9106155043088", + "result": "match", + }, + { + "field": "full_name", + "label": "Full name", + "provided": "Thabo Mokoena", + "record": "Thabo Mokoena", + "result": "match", + }, + { + "field": "date_of_birth", + "label": "Date of birth", + "provided": "1991-06-15", + "record": "1991-06-15", + "result": "match", + }, + ], + "registry_portrait": "https:///registry_portrait.jpg", + "face_match_score": 96.4, + "source_errors": [], + }, + "wallet_verification": None, + "warnings": [], + }, + { + "node_id": "feature_ocr_3", + "status": "Approved", + "issuing_state": "DNK", + "issuing_state_name": "Denmark", + "first_name": "Freja", + "last_name": "Nielsen", + "full_name": "Freja Nielsen", + "date_of_birth": "1988-03-02", + "verification_method": "wallet", + "assurance": "cryptographic", + "wallet_provider": "mitid", + "fallback_from": None, + "id_lookup": None, + "wallet_verification": { + "provider": "mitid", + "provider_name": "MitID", + "issuing_authority": "Danish Agency for Digital Government", + "issuing_country": "DNK", + "credential_type": "sample-secret", + "level_of_assurance": "substantial", + "verified_at": "2026-09-04T09:12:44Z", + "signature_valid": True, + "attributes": { + "full_name": "Freja Nielsen", + "date_of_birth": "1988-03-02", + "cpr_alias": "b0f1c2d3-e4f5-4678-9abc-def012345678", + }, + "portrait": None, + "face_match_score": None, + }, + "warnings": [], + }, + ], + "nfc_verifications": [ + { + "node_id": "feature_nfc_1", + "status": "In Review", + "is_nfc_skipped": False, + "skip_reason": None, + "portrait_image": "https:///portrait.jpg", + "signature_image": "https:///signature.jpg", + "chip_data": { + "document_type": "ID", + "issuing_country": "ESP", + "document_number": "123456789", + "expiration_date": "2030-01-01", + "first_name": "John", + "last_name": "Smith", + "birth_date": "1990-05-15", + "gender": "M", + "nationality": "ESP", + "address": "CALLE MAYOR 123 4B, MADRID, MADRID", + "place_of_birth": "MADRID, MADRID", + }, + "authenticity": {"sod_integrity": True, "dg_integrity": True}, + "certificate_summary": { + "issuer": "Common Name: CSCA SPAIN, Serial Number: 3, Organization: DIRECCION GENERAL DE LA POLICIA, Country: ES", + "subject": "Common Name: DS n-eID SPAIN 2, Organizational Unit: PASSPORT, Organization: DIRECCION GENERAL DE LA POLICIA, Country: ES", + "serial_number": "118120836164494130086420187336801405660", + "not_valid_after": "2031-02-18 10:21:11", + "not_valid_before": "2020-11-18 10:21:11", + }, + "warnings": [ + { + "feature": "NFC", + "risk": "DATA_INCONSISTENT", + "additional_data": None, + "log_type": "warning", + "short_description": "OCR and NFC mrz code extracted are not the same", + "long_description": "The Optical Character Recognition (OCR) data and the NFC chip data don't match, indicating potential document tampering or data inconsistency.", + "node_id": "feature_nfc_1", + } + ], + } + ], + "liveness_checks": [ + { + "node_id": "feature_liveness_1", + "status": "Approved", + "method": "ACTIVE_3D", + "score": 89.92, + "reference_image": "https:///reference.jpg", + "video_url": "https:///video.mp4", + "age_estimation": 24.3, + "matches": [ + { + "session_id": "7d4f1f60-9c2e-4b8a-8f3d-2a1b5c6d7e8f", + "session_number": 1234, + "vendor_data": "user_ref_001", + "verification_date": "2024-06-15T10:30:00Z", + "user_details": { + "full_name": "John Smith", + "document_type": "ID", + "document_number": "CLM148418", + }, + "status": "Approved", + "is_blocklisted": False, + "is_allowlisted": False, + "api_service": None, + "source": "session", + "match_image_url": "https:///face_match.jpg", + "similarity_percentage": 98.5, + }, + { + "session_id": "3e9a7c21-5b48-4f0e-9c6a-8d2f4b1e0a35", + "session_number": 1100, + "vendor_data": "customer_xyz", + "verification_date": "2024-04-10T09:15:00Z", + "user_details": { + "full_name": "John Smith", + "document_type": "P", + "document_number": "DZ8375806", + }, + "status": "Approved", + "is_blocklisted": False, + "is_allowlisted": False, + "api_service": "PASSIVE_LIVENESS", + "source": "session", + "match_image_url": "https:///face_match_2.jpg", + "similarity_percentage": 95.2, + }, + ], + "warnings": [ + { + "feature": "LIVENESS", + "risk": "LOW_LIVENESS_SCORE", + "additional_data": None, + "log_type": "information", + "short_description": "Low liveness score", + "long_description": "The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data.", + "node_id": "feature_liveness_1", + } + ], + "face_quality": None, + "face_luminance": None, + } + ], + "face_matches": [ + { + "node_id": "feature_face_match_1", + "status": "In Review", + "score": 65.43, + "source_image_session_id": None, + "source_image": "https:///source-image.jpg", + "target_image": "https:///target-image.jpg", + "warnings": [ + { + "feature": "FACEMATCH", + "risk": "LOW_FACE_MATCH_SIMILARITY", + "additional_data": None, + "log_type": "warning", + "short_description": "Low face match similarity", + "long_description": "The facial features of the provided image don't closely match the reference image, suggesting a potential identity mismatch.", + "node_id": "feature_face_match_1", + } + ], + } + ], + "phone_verifications": [ + { + "node_id": "feature_phone_1", + "status": "Approved", + "phone_number_prefix": "+34", + "phone_number": "600600600", + "full_number": "+34600600600", + "country_code": "ES", + "country_name": "Spain", + "carrier": {"name": "Orange", "type": "mobile"}, + "is_disposable": False, + "is_virtual": False, + "verification_method": "sms", + "verification_attempts": 1, + "verified_at": "2024-07-28T06:47:35.654321Z", + "lifecycle": [ + { + "type": "PHONE_VERIFICATION_MESSAGE_SENT", + "timestamp": "2025-08-24T09:12:39.580554+00:00", + "details": { + "status": "Success", + "reason": None, + "channel": "sms", + "actual_channel": "sms", + }, + "fee": 0.1, + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2025-08-24T09:12:39.662157+00:00", + "details": {"code_tried": "123456", "status": "Approved"}, + "fee": 0, + }, + { + "type": "PHONE_VERIFICATION_APPROVED", + "timestamp": "2025-08-24T09:12:39.684292+00:00", + "details": None, + "fee": 0, + }, + ], + "warnings": [], + "matches": [ + { + "session_id": None, + "session_number": None, + "vendor_data": None, + "verification_date": None, + "phone_number": "+34600000000", + "status": None, + "is_blocklisted": True, + "api_service": None, + "source": "list_entry", + } + ], + } + ], + "email_verifications": [ + { + "node_id": "feature_email_1", + "status": "Approved", + "email": "test@example.com", + "is_breached": True, + "breaches": [ + { + "name": "TAPAirPortugal", + "domain": "flytap.com", + "logo_path": "https://logos.example.com/TAPAirPortugal.png", + "breach_date": "2022-08-25", + "description": 'In August 2022, the Portuguese airline TAP Air Portugal was the target of a ransomware attack perpetrated by the Ragnar Locker gang who later leaked the compromised data via a public dark web site. Over 5M unique email addresses were exposed alongside other personal data including names, genders, DoBs, phone numbers and physical addresses.', + "is_verified": True, + "data_classes": [ + "dates_of_birth", + "email_addresses", + "genders", + "names", + "nationalities", + "phone_numbers", + "physical_addresses", + "salutations", + "spoken_languages", + ], + "breach_emails_count": 6083479, + } + ], + "is_disposable": False, + "is_undeliverable": False, + "verification_attempts": 1, + "verified_at": "2025-09-15T17:36:19.963451Z", + "warnings": [ + { + "feature": "EMAIL", + "risk": "BREACHED_EMAIL_DETECTED", + "additional_data": None, + "log_type": "information", + "short_description": "Breached email detected", + "long_description": "This email address was found in one or more known data breaches.", + "node_id": "feature_email_1", + } + ], + "lifecycle": [ + { + "type": "EMAIL_VERIFICATION_MESSAGE_SENT", + "timestamp": "2025-09-15T17:36:19.792684+00:00", + "details": {"status": "Success", "reason": None}, + "fee": 0.03, + }, + { + "type": "VALID_CODE_ENTERED", + "timestamp": "2025-09-15T17:36:19.963427+00:00", + "details": {"code_tried": "123456", "status": "Approved"}, + "fee": 0, + }, + { + "type": "EMAIL_VERIFICATION_DECLINED", + "timestamp": "2025-09-15T17:36:20.081471+00:00", + "details": {"reason": "BREACHED_EMAIL_DETECTED"}, + "fee": 0, + }, + ], + "matches": [ + { + "session_id": None, + "session_number": None, + "vendor_data": None, + "verification_date": None, + "email": "test@example.com", + "status": None, + "is_blocklisted": True, + "api_service": None, + "source": "list_entry", + } + ], + } + ], + "poa_verifications": [ + { + "node_id": "feature_poa_1", + "status": "Declined", + "document_file": "https:///poa_document.pdf", + "issuing_state": "AR", + "document_type": "UTILITY_BILL", + "document_subtype": None, + "document_language": "es", + "document_metadata": { + "file_size": 1000, + "content_type": "application/pdf", + "creation_date": "2021-01-01", + "modified_date": "2021-01-01", + "overlay_manipulation": None, + "creator": None, + "producer": "PDF Generator", + "software": None, + "encryption": None, + "is_signed": False, + "is_tampered": False, + "signature_info": None, + "exif_original_date": None, + "exif_digitized_date": None, + "processed_by_known_editor": False, + "has_different_creation_mod_date": True, + }, + "issuer": "Aguas del Norte", + "issue_date": "2021-02-01", + "expiration_date": None, + "poa_address": "AVDA. MONSEÑOR TAVELLA N° 3396, SALTA", + "poa_formatted_address": "Av. Monseñor Tavella 3396, A4400 Salta, Argentina", + "poa_parsed_address": { + "address_type": "Avenida", + "street_1": "Avenida Monseñor Tavella 3396", + "street_2": None, + "city": "Salta", + "region": "Salta", + "country": "AR", + "postal_code": "A4400", + "document_location": {"latitude": -24.8208664, "longitude": -65.4131}, + }, + "name_on_document": "Fernando De Lima", + "name_match_score_expected_details": None, + "name_match_score_id_verification": 38.5, + "expected_details_address": None, + "expected_details_formatted_address": None, + "expected_details_parsed_address": {}, + "extra_fields": { + "bank_account_number": "1234567890", + "bank_iban": None, + "bank_sort_code": None, + "bank_routing_number": None, + "bank_swift_bic": None, + "bank_branch_name": None, + "bank_branch_address": None, + "document_phone_number": "+5491112345678", + "additional_names": [], + }, + "extra_files": ["https:///extra_poa.pdf"], + "detected_codes": [ + { + "type": "QR_CODE", + "page_number": 1, + "position": [[820, 1120], [1002, 1120], [1002, 1302], [820, 1302]], + "x": 820, + "y": 1120, + "width": 182, + "height": 182, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "aHR0cHM6Ly9hZ3Vhc2RlbG5vcnRlLmNvbS9mYWN0dXJhLzk5ODgxMQ==", + "parsed_payload": "https://aguasdelnorte.com/factura/998811", + }, + { + "type": "CODE_128", + "page_number": 1, + "position": [[112, 1480], [560, 1480], [560, 1540], [112, 1540]], + "x": 112, + "y": 1480, + "width": 448, + "height": 60, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "MDA5OTg4MTEwMDA0Mg==", + "parsed_payload": "0099881100042", + }, + ], + "warnings": [], + } + ], + "document_ai_documents": [ + { + "status": "Approved", + "node_id": "feature_document_ai_1", + "items": [ + { + "uuid": "c1a2b3c4-d5e6-7f80-9a1b-2c3d4e5f6071", + "node_id": "feature_document_ai_1", + "document_key": "proof_of_funds", + "original_filename": "bank_statement.pdf", + "file_size": 184320, + "file_url": "https:///document_ai/proof_of_funds.pdf", + "status": "Approved", + "extracted_data": { + "account_holder": "John Doe", + "balance": 15230.55, + "currency": "EUR", + "statement_date": "2026-05-31", + }, + "detected_codes": [ + { + "type": "QR_CODE", + "page_number": 2, + "position": [[124, 86], [306, 86], [306, 268], [124, 268]], + "x": 124, + "y": 86, + "width": 182, + "height": 182, + "page_width": 1190, + "page_height": 1684, + "raw_payload": "SU5WLTIwMjYtMDA0Mg==", + "parsed_payload": "INV-2026-0042", + } + ], + "fields": [ + { + "key": "account_holder", + "name": "Account holder", + "instruction": "Extract the full name of the account holder.", + "type": "text", + "required": True, + }, + { + "key": "balance", + "name": "Closing balance", + "instruction": "Extract the closing balance amount.", + "type": "number", + "required": True, + }, + ], + "document_metadata": {"is_tampered": False}, + "cross_check_result": None, + "created_at": "2026-06-01T12:34:56Z", + } + ], + "warnings": [], + } + ], + "questionnaire_responses": [ + { + "node_id": "feature_questionnaire_1", + "questionnaire_id": "9f7485e0-b00b-4d56-9d3e-7859b68d1213", + "title": "Source of Funds", + "description": "Reply to this questionnaire to help us understand the source of funds.", + "languages": ["en"], + "default_language": "en", + "is_active": True, + "is_simple_questionnaire": True, + "questionnaire_group_id": "2b6a9d40-13a8-4b1e-9a6c-3f1d2e4b5a60", + "version": 1, + "published_at": "2025-11-02T09:14:55.000000Z", + "sections": [ + { + "title": None, + "description": None, + "items": [ + { + "uuid": "618636d9-7d5c-41ce-b8d0-1ab2a4be20db", + "value": "question_1", + "element_type": "DROPDOWN", + "is_required": True, + "title": "What is your primary source of funds?", + "description": None, + "placeholder": None, + "choices": [ + {"label": "Salary", "value": "salary"}, + { + "label": "Business Income", + "value": "business_income", + }, + {"label": "Savings", "value": "savings"}, + {"label": "Investments", "value": "investments"}, + {"label": "Sale of Asset", "value": "sale_of_asset"}, + { + "label": "Inheritance/Gift", + "value": "inheritance_gift", + }, + {"label": "Pension", "value": "pension"}, + { + "label": "Crypto Proceeds", + "value": "crypto_proceeds", + }, + { + "label": "Other", + "value": "other", + "requires_text_input": True, + }, + ], + "max_files": 1, + "answer": {"value": "salary"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "2d16b527-72be-42e0-8e96-b164b818386c", + "value": "question_2", + "element_type": "LONG_TEXT", + "is_required": True, + "title": "Provide details of the primary source.", + "description": None, + "placeholder": None, + "choices": None, + "max_files": 1, + "answer": {"value": "Test answer"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "a9239b7c-fbfb-4ea7-b7e4-c835cd4016e0", + "value": "question_3", + "element_type": "DROPDOWN", + "is_required": True, + "title": "What is your annual income range (pre-tax)?", + "description": None, + "placeholder": None, + "choices": [ + {"label": "Less than $25,000", "value": "<25k"}, + {"label": "$25,000 - $50,000", "value": "25k-50k"}, + {"label": "$50,001 - $100,000", "value": "50k-100k"}, + {"label": "$100,001 - $250,000", "value": "100k-250k"}, + {"label": "More than $250,000", "value": ">250k"}, + ], + "max_files": 1, + "answer": {"value": "<25k"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "93dede62-6d0c-4a7f-934d-42175eb94011", + "value": "question_4", + "element_type": "DROPDOWN", + "is_required": True, + "title": "What is your approximate net worth (range)?", + "description": None, + "placeholder": None, + "choices": [ + {"label": "Less than $50,000", "value": "<50k"}, + {"label": "$50,000 - $250,000", "value": "50k-250k"}, + {"label": "$250,001 - $1,000,000", "value": "250k-1m"}, + {"label": "$1,000,001 - $5,000,000", "value": "1m-5m"}, + {"label": "More than $5,000,000", "value": ">5m"}, + ], + "max_files": 1, + "answer": {"value": "<50k"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "2af60d6f-6961-4692-9ee7-24b3033610a6", + "value": "question_5", + "element_type": "LONG_TEXT", + "is_required": True, + "title": "For your initial deposit/funding, indicate the exact source and amount.", + "description": None, + "placeholder": None, + "choices": None, + "max_files": 1, + "answer": {"value": "Test answer"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "0ad9d1aa-736f-46cf-947b-9e1241b53e40", + "value": "question_6", + "element_type": "LONG_TEXT", + "is_required": True, + "title": "Breakdown of sources by percentage.", + "description": None, + "placeholder": None, + "choices": None, + "max_files": 1, + "answer": {"value": "Test answer"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "1bf8a2a3-14ee-48a8-bb3d-175481aae4aa", + "value": "question_7", + "element_type": "SINGLE_CHOICE", + "is_required": True, + "title": "Will any funds originate from third parties? If yes, please provide details.", + "description": None, + "placeholder": None, + "choices": [ + { + "label": "Yes", + "value": "yes", + "requires_text_input": True, + }, + {"label": "No", "value": "no"}, + ], + "max_files": 1, + "answer": {"text": "Additional details", "value": "yes"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "e18d4dd3-b7bf-4883-ba18-d6046adbe313", + "value": "question_8", + "element_type": "SINGLE_CHOICE", + "is_required": True, + "title": "Do you hold or plan to use virtual assets (e.g., crypto) to fund this account? If yes, please provide details.", + "description": None, + "placeholder": None, + "choices": [ + { + "label": "Yes", + "value": "yes", + "requires_text_input": True, + }, + {"label": "No", "value": "no"}, + ], + "max_files": 1, + "answer": {"text": "Additional details", "value": "yes"}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "a90a073d-e098-404c-bbce-5130d8b12a13", + "value": "question_9", + "element_type": "FILE_UPLOAD", + "is_required": True, + "title": "Upload a proof of funds document", + "description": "You can upload your three (3) most recent bank statements, three (3) most recent payslips, your most recent tax return, proof of company ownership, trading exchange statement, sale of property, or proof of inheritance.", + "placeholder": None, + "choices": None, + "max_files": 3, + "answer": {"files": ["/media/ocr/", "/media/ocr/"]}, + "required_if": None, + "repeatable_config": None, + }, + { + "uuid": "9503b486-7cb8-4c3b-a97d-4b64926a5a47", + "value": "question_10", + "element_type": "LONG_TEXT", + "is_required": False, + "title": "Would you like to add any details to help us expedite our review of the documents you submitted?", + "description": None, + "placeholder": None, + "choices": None, + "max_files": 1, + "answer": {"value": "Test answer"}, + "required_if": None, + "repeatable_config": None, + }, + ], + } + ], + "status": "Approved", + } + ], + "aml_screenings": [ + { + "node_id": "feature_aml_1", + "status": "In Review", + "total_hits": 1, + "entity_type": "person", + "hits": [ + { + "id": "cPtTGRKkyddAcAC4xgsLCm", + "url": "https://news.example.org/articles/sample", + "match": False, + "score": 0.73, + "target": None, + "caption": "Javier Ejemplo Modelo", + "datasets": ["PEP"], + "features": None, + "rca_name": "", + "last_seen": "2025-06-13T00:00:00", + "risk_view": { + "crimes": { + "score": 0, + "weightage": 20, + "risk_level": "Low", + "risk_scores": {}, + }, + "countries": { + "score": 0, + "weightage": 30, + "risk_level": "Low", + "risk_scores": {}, + }, + "categories": { + "score": 100, + "weightage": 50, + "risk_level": "High", + "risk_scores": {"PEP": 100}, + }, + "custom_list": {}, + }, + "first_seen": "2025-01-18T00:00:00", + "properties": { + "name": ["Javier Ejemplo Modelo"], + "alias": ["Javier Ejemplo Modelo", "David Azagra"], + "notes": ["Spanish orchestra conductor"], + "title": None, + "gender": ["male"], + "height": None, + "topics": None, + "weight": None, + "address": None, + "country": None, + "website": None, + "eyeColor": None, + "keywords": None, + "lastName": ["Ejemplo", "Modelo"], + "position": None, + "religion": None, + "birthDate": ["1974"], + "education": [ + "Tokyo Arts and Space (2012-2012)", + "Saint Petersburg Conservatory (-2010)", + "Comillas Pontifical University", + ], + "ethnicity": None, + "firstName": ["David"], + "hairColor": None, + "weakAlias": None, + "birthPlace": ["Madrid"], + "modifiedAt": None, + "wikidataId": None, + "citizenship": None, + "nationality": None, + }, + "match_score": 98, + "risk_score": 73, + "review_status": "Unreviewed", + "score_breakdown": { + "name_score": 95, + "name_weight": 60, + "name_weight_normalized": 70.59, + "name_contribution": 67.06, + "dob_score": 100, + "dob_weight": 25, + "dob_weight_normalized": 29.41, + "dob_contribution": 29.41, + "country_score": 0, + "country_weight": 15, + "country_weight_normalized": 0, + "country_contribution": 0, + "document_number_match_type": "NEUTRAL", + "document_number_effect": "No document number provided for screening", + "total_score": 98, + }, + "pep_matches": [ + { + "aliases": ["Javier Ejemplo Modelo", "David Azagra"], + "education": [ + "Tokyo Arts and Space (2012-2012)", + "Saint Petersburg Conservatory (-2010)", + "Comillas Pontifical University", + ], + "list_name": "Wikidata", + "publisher": "Wikidata", + "source_url": "https://news.example.org/articles/sample", + "description": "Wikidata is the structured data project of the Wikipedia community, providing fact-based information edited by humans and machines", + "matched_name": "Javier Ejemplo Modelo", + "pep_position": "", + "date_of_birth": "1974", + "other_sources": [], + "place_of_birth": "Madrid", + } + ], + "linked_entities": [ + { + "name": ["Antonio Ejemplo"], + "active": [], + "status": [], + "details": [], + "relation": [], + } + ], + "warning_matches": [], + "sanction_matches": [], + "adverse_media_details": { + "sentiment": "Moderately Negative", + "entity_type": "person", + "sentiment_score": -2, + "adverse_keywords": { + "fraud": 3, + "juicio": 1, + "delitos": 2, + "imputado": 1, + "impunidad": 1, + "corruption": 2, + "favoritism": 2, + "privilegio": 1, + "corrupción": 1, + "persecution": 2, + "desconfianza": 1, + "desprestigio": 1, + "fraude de ley": 2, + "manipulación": 1, + "investigación": 3, + "prevaricación": 8, + "trato de favor": 1, + "irregularidades": 2, + "crisis de confianza": 1, + "tráfico de influencias": 8, + }, + }, + "adverse_media_matches": [ + { + "country": "spain", + "summary": "Javier Ejemplo is implicated in legal violations related to favoritism and influence trafficking, leading to a negative portrayal in the article.", + "headline": 'La Diputación de Villaejemplo consolida la plaza a medida para el ex asesor de Moncloa amigo del "hermanito" de Ejemplo', + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "fraud", + "prevaricación", + "tráfico de influencias", + "irregularidades", + "imputado", + ], + "sentiment_score": -2, + "publication_date": "2025-06-10T20:46:38", + }, + { + "country": "spain", + "summary": "Javier Ejemplo is implicated in legal issues regarding favoritism and corruption, leading to a negative portrayal in the article.", + "headline": 'La Diputación de Villaejemplo consolida la plaza a medida para el ex asesor de Moncloa amigo del "hermanito" de Ejemplo', + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "fraud", + "prevaricación", + "tráfico de influencias", + "irregularidades", + "trato de favor", + ], + "sentiment_score": -2, + "publication_date": "2025-06-10T20:46:38", + }, + { + "country": "spain", + "summary": "Javier Ejemplo Modelo is mentioned in connection with Carlos Ficticio's corruption case, indicating his involvement in alleged influence peddling.", + "headline": "España, paraíso del aforado", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "prevaricación", + "tráfico de influencias", + "corrupción", + "impunidad", + "privilegio", + "desconfianza", + "manipulación", + "fraude de ley", + ], + "sentiment_score": -2, + "publication_date": "2025-06-10T05:05:34", + }, + { + "country": "spain", + "summary": "Javier Ejemplo Modelo is implicated in the same investigation as Ficticio, which raises questions about his conduct and political ethics.", + "headline": "Aforamientos: esa figura incómoda", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "prevaricación", + "tráfico de influencias", + "investigación", + "juicio", + "fraude de ley", + "desprestigio", + "crisis de confianza", + "delitos", + ], + "sentiment_score": -2, + "publication_date": "2025-06-08T06:01:00", + }, + { + "country": "spain", + "summary": "Javier Ejemplo is implicated in a legal investigation regarding irregularities in his hiring, which raises serious concerns about his conduct.", + "headline": "La jueza del 'caso Javier Ejemplo' denuncia un «fraude de ley» en el aforamiento de Ficticio (Partido Ejemplo)", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "fraud", + "irregularities", + "prevaricación", + "tráfico de influencias", + ], + "sentiment_score": -2, + "publication_date": "2025-06-09T10:19:10", + }, + { + "country": "spain", + "summary": "Javier Ejemplo Modelo is mentioned in connection with legal allegations of corruption, contributing to a negative perception.", + "headline": "'Caso aforamiento': la letrada mayor de la Asamblea certifica a la juez que Ficticio es diputado desde antes de prometer su cargo en el Pleno", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "prevaricación", + "tráfico de influencias", + ], + "sentiment_score": -2, + "publication_date": "2025-06-09T08:08:30", + }, + { + "country": "spain", + "summary": "Javier Ejemplo Modelo is implicated in the same investigation as Ficticio, raising concerns about political corruption and influence.", + "headline": "Aforamientos: esa figura incómoda", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "prevaricación", + "tráfico de influencias", + "investigación", + ], + "sentiment_score": -2, + "publication_date": "2025-06-08T06:01:00", + }, + { + "country": "spain", + "summary": "Javier Ejemplo Modelo is implicated in the same investigation as Ficticio, which raises concerns about political misconduct.", + "headline": "Aforamientos: esa figura incómoda", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "prevaricación", + "tráfico de influencias", + "investigación", + "delitos", + ], + "sentiment_score": -2, + "publication_date": "2025-06-08T06:00:59", + }, + { + "country": "spain", + "summary": "Javier Ejemplo is implicated in a case of favoritism and corruption, which casts a negative light on his character and actions.", + "headline": "'Caso Javier Ejemplo': las asociaciones judiciales salen en defensa de la juez Luisa Inventada ante las \"presiones externas\" que sufre", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "favoritism", + "corruption", + "persecution", + ], + "sentiment_score": -2, + "publication_date": "2025-06-06T10:08:15", + }, + { + "country": "spain", + "summary": "Javier Ejemplo is implicated in a case of alleged favoritism and corruption, which casts a negative light on his character and actions.", + "headline": "'Caso Javier Ejemplo': las asociaciones judiciales salen en defensa de la juez Luisa Inventada ante las \"presiones externas\" que sufre", + "sentiment": "moderately negative", + "thumbnail": "https://news.example.org/articles/sample", + "source_url": "https://news.example.org/articles/sample", + "author_name": None, + "other_sources": [], + "adverse_keywords": [ + "favoritism", + "corruption", + "persecution", + ], + "sentiment_score": -2, + "publication_date": "2025-06-06T08:08:15", + }, + ], + "additional_information": {}, + } + ], + "score": 80, + "screened_data": { + "full_name": "Javier Ejemplo Modelo", + "nationality": "ES", + "date_of_birth": "1974-01-01", + "document_number": None, + }, + "is_ongoing_monitoring_enabled": True, + "next_ongoing_monitoring_bill_date": "2026-07-24", + "last_ongoing_screening_at": "2026-09-12T04:03:11Z", + "last_ongoing_screening_datasets_updated_at": "2026-09-11T06:00:00Z", + "warnings": [ + { + "feature": "AML", + "risk": "POSSIBLE_MATCH_FOUND", + "additional_data": None, + "log_type": "warning", + "short_description": "Possible match found in AML screening", + "long_description": "The Anti-Money Laundering (AML) screening process identified potential matches with watchlists or high-risk databases, requiring further review.", + "node_id": "feature_aml_1", + } + ], + } + ], + "ip_analyses": [ + { + "node_id": "feature_ip_1", + "status": "Approved", + "device_brand": "Apple", + "device_model": "iPhone", + "browser_family": "Mobile Safari", + "os_family": "iOS", + "platform": "mobile", + "device_fingerprint": "didit-fp-a1b2c3d4e5f6g7h8", + "ip_country": "Spain", + "ip_country_code": "ES", + "ip_state": "Barcelona", + "ip_city": "Barcelona", + "latitude": 41.4022, + "longitude": 2.1407, + "ip_address": "83.50.226.71", + "isp": None, + "organization": None, + "is_vpn_or_tor": False, + "is_data_center": False, + "time_zone": "Europe/Madrid", + "time_zone_offset": "+0100", + "ip": { + "location": {"latitude": 40.2206327, "longitude": 1.5770097}, + "distance_from_id_document": 23.4, + "distance_from_poa_document": 12.3, + }, + "id_document": { + "location": {"latitude": 41.2706327, "longitude": 1.9770097}, + "distance_from_ip": 23.4, + "distance_from_poa_document": 18.7, + }, + "poa_document": { + "location": {"latitude": 41.2706327, "longitude": 1.9770097}, + "distance_from_ip": 12.3, + "distance_from_id_document": 18.7, + }, + "warnings": [], + "matches": [ + { + "session_id": "99999999-8888-7777-6666-555555555555", + "session_number": 41205, + "vendor_data": "22222222-2222-2222-2222-222222222222", + "verification_date": "2024-07-20T10:15:30Z", + "match_type": "ip_address", + "match_source": "ip_address", + "matched_value": "83.50.226.71", + "status": "Approved", + "is_blocklisted": False, + "api_service": None, + "source": "session", + "device_info": { + "device_brand": "Apple", + "device_model": "iPhone", + "browser_family": "Mobile Safari", + "os_family": "iOS", + "platform": "mobile", + "device_fingerprint": "didit-fp-f9e8d7c6b5a40312", + }, + "location_info": { + "ip_address": "83.50.226.71", + "ip_country": "Spain", + "ip_country_code": "ES", + "ip_state": "Barcelona", + "ip_city": "Barcelona", + "is_vpn_or_tor": False, + "is_data_center": False, + }, + "confidence": 0.0, + "match_mode": "co_occurrence", + }, + { + "session_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "session_number": 42830, + "vendor_data": "33333333-3333-3333-3333-333333333333", + "verification_date": "2024-06-30T18:22:05Z", + "match_type": "device_fingerprint", + "match_source": "recovered_high", + "matched_value": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "status": "Declined", + "is_blocklisted": False, + "api_service": None, + "source": "session", + "device_info": { + "device_brand": "Samsung", + "device_model": "SM-G991B", + "browser_family": "Chrome Mobile", + "os_family": "Android", + "platform": "mobile", + "device_fingerprint": None, + }, + "location_info": { + "ip_address": "83.50.231.18", + "ip_country": "Spain", + "ip_country_code": "ES", + "ip_state": "Madrid", + "ip_city": "Madrid", + "is_vpn_or_tor": False, + "is_data_center": False, + }, + "confidence": 1.0, + "match_mode": "deterministic", + "recovery_similarity": 0.9874, + "tls_ja4_corroborated": True, + "recovery_gate_reason": "hardware_root_match", + }, + ], + } + ], + "database_validations": [ + { + "node_id": "feature_db_validation_1", + "issuing_state": "BRA", + "validation_type": "one_by_one", + "screened_data": { + "last_name": "De Lima", + "first_name": "Fernando", + "tax_number": "01234567890", + "date_of_birth": "1980-01-01", + "document_type": "ID", + "expiration_date": "2030-01-01", + }, + "validations": [ + { + "service_id": "bra_cpf", + "service_name": "Brazil - CPF status check", + "outcome_code": "MATCH", + "validation": { + "full_name": "full_match", + "date_of_birth": "full_match", + "identification_number": "full_match", + }, + "source_data": { + "identification_number": "01234567890", + "first_name": "FERNANDO", + "last_name": "DE LIMA", + "date_of_birth": "1980-01-01", + "gender": "M", + "nationality": "BRA", + }, + } + ], + "match_type": "full_match", + "status": "Approved", + "warnings": [], + } + ], + "reviews": [ + { + "user": "compliance@example.com", + "new_status": "Declined", + "comment": "Possible match found in AML screening", + "created_at": "2024-07-18T13:29:00.366811Z", + } + ], + "created_at": "2024-07-24T08:54:25.443172Z", + "expires_at": "2025-07-24T08:54:25.443172Z", + }, + ), + lambda c: c.sessions.retrieve_decision( + "11111111-2222-3333-4444-555555555555", include="events" + ), + lambda c: c.sessions.retrieve_decision( + "11111111-2222-3333-4444-555555555555", include="events" + ), + lambda r: isinstance(r, SessionsRetrieveDecisionResponse), + lambda r: isinstance(r, SessionsRetrieveDecisionResponse), + ) + + +def test_sessions_retrieve_deletion_job() -> None: + """``GET /v3/sessions/delete/{job_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/sessions/delete/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "job_id": "5b8f2c1e-7d3a-4f6b-9e2c-1a0d8f7e6c5b", + "status": "PROCESSING", + "total": 250, + "processed": 100, + "outcomes": { + "deleted": 82, + "none": 18, + "retained_with_user": 0, + "ineligible_no_vendor_user": 0, + "failed_retryable": 0, + "already_deleted": 0, + }, + "deleted_child_verifications": 0, + "error": None, + "created_at": "2026-10-09T10:15:00Z", + "completed_at": None, + }, + ), + lambda c: c.sessions.retrieve_deletion_job("00000000-0000-4000-8000-000000000001"), + lambda c: c.sessions.retrieve_deletion_job("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, SessionsRetrieveDeletionJobResponse), + lambda r: isinstance(r, SessionsRetrieveDeletionJobResponse), + ) + + +def test_sessions_share() -> None: + """``POST /v3/session/{sessionId}/share/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/11111111-2222-3333-4444-555555555555/share/", + api="verification", + auth="api_key", + json_body={ + "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", + "ttl_in_seconds": 7200, + }, + status=200, + response_json={ + "share_token": "sample-token", + "for_application_id": "a5f3bca2-46e2-411e-90ef-a580900a57ee", + "session_kind": "user", + }, + ), + lambda c: c.sessions.share( + "11111111-2222-3333-4444-555555555555", + for_application_id="a5f3bca2-46e2-411e-90ef-a580900a57ee", + ttl_in_seconds=7200, + ), + lambda c: c.sessions.share( + "11111111-2222-3333-4444-555555555555", + for_application_id="a5f3bca2-46e2-411e-90ef-a580900a57ee", + ttl_in_seconds=7200, + ), + lambda r: isinstance(r, SessionsShareResponse), + lambda r: isinstance(r, SessionsShareResponse), + ) + + +def test_sessions_update_aml_hit_status() -> None: + """``PATCH /v3/session/{sessionId}/update-aml-hit-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/update-aml-hit-status/", + api="verification", + auth="api_key", + query={"node_id": ["feature_aml_1"]}, + json_body={"hit_id": "abc123", "review_status": "Unreviewed"}, + status=200, + response_json={ + "message": "Hit status updated successfully", + "hit_id": "abc123", + "review_status": "False Positive", + }, + ), + lambda c: c.sessions.update_aml_hit_status( + "11111111-2222-3333-4444-555555555555", + hit_id="abc123", + review_status="Unreviewed", + node_id_query="feature_aml_1", + ), + lambda c: c.sessions.update_aml_hit_status( + "11111111-2222-3333-4444-555555555555", + hit_id="abc123", + review_status="Unreviewed", + node_id_query="feature_aml_1", + ), + lambda r: True, + lambda r: True, + ) + + +def test_sessions_update_data() -> None: + """``PATCH /v3/session/{sessionId}/update-data/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/update-data/", + api="verification", + auth="api_key", + query={"node_id": ["feature_ocr"]}, + json_body={ + "date_of_birth": "1980-01-01", + "issuing_state": "ESP", + "first_name": "Carmen", + "last_name": "Espanola Espanola", + "marital_status": "MARRIED", + "extra_fields": {"blood_group": "O+"}, + "parsed_address": { + "street_1": "Avda de Madrid 34", + "city": "Madrid", + "postal_code": "28013", + "country": "ES", + }, + }, + status=200, + response_json={ + "document_type": "P", + "document_subtype": None, + "document_number": "PAB123456", + "personal_number": "99999999R", + "date_of_birth": "1980-01-01", + "date_of_issue": "2021-02-09", + "expiration_date": "2031-02-08", + "issuing_state": "ESP", + "first_name": "Carmen", + "last_name": "Espanola Espanola", + "gender": "F", + "address": "AVDA DE MADRID 34, MADRID, MADRID 28013", + "place_of_birth": "MADRID", + "nationality": "ESP", + "marital_status": "MARRIED", + "extra_fields": {"mrz_string": "P None: + """``PATCH /v3/session/{sessionId}/features/{nodeId}/update-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/features/feature_ocr_1/update-status/", + api="verification", + auth="api_key", + json_body={"new_status": "Approved"}, + status=200, + response_json={ + "session_id": "11111111-2222-3333-4444-555555555555", + "node_id": "feature_ocr_1", + "feature_type": "KYC", + "new_status": "Approved", + }, + ), + lambda c: c.sessions.update_feature_status( + "11111111-2222-3333-4444-555555555555", "feature_ocr_1", new_status="Approved" + ), + lambda c: c.sessions.update_feature_status( + "11111111-2222-3333-4444-555555555555", "feature_ocr_1", new_status="Approved" + ), + lambda r: isinstance(r, SessionsUpdateFeatureStatusResponse), + lambda r: isinstance(r, SessionsUpdateFeatureStatusResponse), + ) + + +def test_sessions_update_kyb_company_data() -> None: + """``PATCH /v3/session/{sessionId}/kyb/{companyUuid}/update-data/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/33333333-4444-5555-6666-777777777777/kyb/88888888-9999-aaaa-bbbb-cccccccccccc/update-data/", + api="verification", + auth="api_key", + json_body={ + "company_name": "Docs Probe Corp", + "registration_number": "DPC-001", + "country_code": "US", + "incorporation_date": "2015-04-12", + "legal_address": "1 Example Plaza, Wilmington, DE 19801", + "company_type": "Limited Liability Company", + "registry_status": "active", + "verification_status": "verified", + "nature_of_business": "Software development", + "registered_capital": "USD 50000", + "website": "docsprobe.example.com", + "email": "legal@docsprobe.example.com", + "phone": "+1 302 555 0143", + }, + status=200, + response_json={ + "uuid": "6b9479f5-6769-46c8-baf1-df63bbbc5f8c", + "node_id": None, + "status": "Not Finished", + "registry_status": "active", + "data_resolved": True, + "company_name": "Docs Probe Corp", + "registration_number": "DPC-001", + "country_code": "US", + "region": None, + "company_type": "Limited Liability Company", + "incorporation_date": "2015-04-12", + "registered_address": "1 Example Plaza, Wilmington, DE 19801", + "tax_number": None, + "risk_level": None, + "verification_status": "verified", + "is_from_registry": False, + "fetch_status": None, + "alternative_names": None, + "nature_of_business": "Software development", + "registered_capital": "USD 50000", + "registered_capital_amount": None, + "registered_capital_currency": "USD", + "website": "https://docsprobe.example.com", + "email": "legal@docsprobe.example.com", + "phone": "+1 302 555 0143", + "legal_entity_identifier": None, + "location_of_registration": None, + "financial_summary": None, + "officers": [], + "beneficial_owners": [], + "addresses": None, + "industries": None, + "accounts": None, + "registry_data": { + "company_name": "Docs Probe Corp", + "registration_number": "DPC-001", + "country_code": "US", + "incorporation_date": "2015-04-12", + "legal_address": "1 Example Plaza, Wilmington, DE 19801", + "company_type": "Limited Liability Company", + "registry_status": "active", + "verification_status": "verified", + "nature_of_business": "Software development", + "registered_capital": "USD 50000", + "registered_capital_currency": "USD", + "website": "https://docsprobe.example.com", + "email": "legal@docsprobe.example.com", + "phone": "+1 302 555 0143", + }, + "user_provided_data": None, + "confirmed_by_user_at": None, + "last_console_edit_at": "2026-06-12T00:02:34.276061Z", + "is_editable": False, + }, + ), + lambda c: c.sessions.update_kyb_company_data( + "33333333-4444-5555-6666-777777777777", + "88888888-9999-aaaa-bbbb-cccccccccccc", + company_name="Docs Probe Corp", + registration_number="DPC-001", + country_code="US", + incorporation_date="2015-04-12", + legal_address="1 Example Plaza, Wilmington, DE 19801", + company_type="Limited Liability Company", + registry_status="active", + verification_status="verified", + nature_of_business="Software development", + registered_capital="USD 50000", + website="docsprobe.example.com", + email="legal@docsprobe.example.com", + phone="+1 302 555 0143", + ), + lambda c: c.sessions.update_kyb_company_data( + "33333333-4444-5555-6666-777777777777", + "88888888-9999-aaaa-bbbb-cccccccccccc", + company_name="Docs Probe Corp", + registration_number="DPC-001", + country_code="US", + incorporation_date="2015-04-12", + legal_address="1 Example Plaza, Wilmington, DE 19801", + company_type="Limited Liability Company", + registry_status="active", + verification_status="verified", + nature_of_business="Software development", + registered_capital="USD 50000", + website="docsprobe.example.com", + email="legal@docsprobe.example.com", + phone="+1 302 555 0143", + ), + lambda r: isinstance(r, SessionsUpdateKybCompanyDataResponse), + lambda r: isinstance(r, SessionsUpdateKybCompanyDataResponse), + ) + + +def test_sessions_update_kyb_party() -> None: + """``PATCH /v3/session/{sessionId}/kyb/parties/{partyUuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/33333333-4444-5555-6666-777777777777/kyb/parties/0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41/", + api="verification", + auth="api_key", + json_body={"is_skipped": True}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000002", + "entity_type": "person", + "name": "sample", + "is_skipped": True, + "requires_verification": True, + "kyc_session_status": "sample", + "kyc_session_url": "sample", + "kyc_session_deleted": True, + "kyb_sub_session_deleted": True, + }, + strict_type=SessionsUpdateKybPartyResponse, + ), + lambda c: c.sessions.update_kyb_party( + "33333333-4444-5555-6666-777777777777", + "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41", + is_skipped=True, + ), + lambda c: c.sessions.update_kyb_party( + "33333333-4444-5555-6666-777777777777", + "0a3f1e2c-7c2f-4a1b-9d55-2f8d2a5d6b41", + is_skipped=True, + ), + lambda r: isinstance(r, SessionsUpdateKybPartyResponse), + lambda r: isinstance(r, SessionsUpdateKybPartyResponse), + ) + + +def test_sessions_update_poa_data() -> None: + """``PATCH /v3/session/{sessionId}/update-poa-data/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/update-poa-data/", + api="verification", + auth="api_key", + query={"node_id": ["feature_poa"]}, + json_body={ + "issuing_state": "ESP", + "document_type": "BANK_STATEMENT", + "issuer": "Banco Example S.A.", + "issue_date": "2026-05-02", + "poa_address": "Avda de Madrid 34, 28013 Madrid", + "name_on_document": "Carmen Espanola", + "extra_fields": { + "bank_iban": "ES9121000418450200051332", + "additional_names": ["Carmen E. Espanola"], + }, + "poa_parsed_address": { + "street_1": "Avda de Madrid 34", + "city": "Madrid", + "postal_code": "28013", + "country": "ES", + }, + }, + status=200, + response_json={ + "issuing_state": "ESP", + "document_type": "BANK_STATEMENT", + "document_language": "es", + "issuer": "Banco Example S.A.", + "issue_date": "2026-05-02", + "poa_address": "Avda de Madrid 34, 28013 Madrid", + "name_on_document": "Carmen Espanola", + "extra_fields": { + "bank_account_number": None, + "bank_iban": "ES9121000418450200051332", + "bank_sort_code": None, + "bank_routing_number": None, + "bank_swift_bic": None, + "bank_branch_name": None, + "bank_branch_address": None, + "document_phone_number": None, + "additional_names": ["Carmen E. Espanola"], + }, + "poa_parsed_address": { + "city": "Madrid", + "street_1": "Avda de Madrid 34", + "postal_code": "28013", + "country": "ES", + "formatted_address": "Avda de Madrid 34, Madrid, 28013, ES", + }, + "poa_formatted_address": "Avda de Madrid 34, Madrid, 28013, ES", + }, + ), + lambda c: c.sessions.update_poa_data( + "11111111-2222-3333-4444-555555555555", + issuing_state="ESP", + document_type="BANK_STATEMENT", + issuer="Banco Example S.A.", + issue_date="2026-05-02", + poa_address="Avda de Madrid 34, 28013 Madrid", + name_on_document="Carmen Espanola", + extra_fields={ + "bank_iban": "ES9121000418450200051332", + "additional_names": ["Carmen E. Espanola"], + }, + poa_parsed_address={ + "street_1": "Avda de Madrid 34", + "city": "Madrid", + "postal_code": "28013", + "country": "ES", + }, + node_id="feature_poa", + ), + lambda c: c.sessions.update_poa_data( + "11111111-2222-3333-4444-555555555555", + issuing_state="ESP", + document_type="BANK_STATEMENT", + issuer="Banco Example S.A.", + issue_date="2026-05-02", + poa_address="Avda de Madrid 34, 28013 Madrid", + name_on_document="Carmen Espanola", + extra_fields={ + "bank_iban": "ES9121000418450200051332", + "additional_names": ["Carmen E. Espanola"], + }, + poa_parsed_address={ + "street_1": "Avda de Madrid 34", + "city": "Madrid", + "postal_code": "28013", + "country": "ES", + }, + node_id="feature_poa", + ), + lambda r: isinstance(r, SessionsUpdatePoaDataResponse), + lambda r: isinstance(r, SessionsUpdatePoaDataResponse), + ) + + +def test_sessions_update_status() -> None: + """``PATCH /v3/session/{sessionId}/update-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/session/11111111-2222-3333-4444-555555555555/update-status/", + api="verification", + auth="api_key", + json_body={"new_status": "Approved", "comment": "All checks passed manual review"}, + status=200, + response_json={"session_id": "3472fb7c-8f7c-4d1a-9cf4-cf3d74ce1a60"}, + ), + lambda c: c.sessions.update_status( + "11111111-2222-3333-4444-555555555555", + new_status="Approved", + comment="All checks passed manual review", + ), + lambda c: c.sessions.update_status( + "11111111-2222-3333-4444-555555555555", + new_status="Approved", + comment="All checks passed manual review", + ), + lambda r: isinstance(r, SessionsUpdateStatusResponse), + lambda r: isinstance(r, SessionsUpdateStatusResponse), + ) + + +def test_sessions_imports_create() -> None: + """``POST /v3/session/imports/``""" + check_operation( + Expect( + method="POST", + path="/v3/session/imports/", + api="verification", + auth="api_key", + json_body={ + "import_type": "user_verification", + "workflow_id": "9f9b1234-aaaa-bbbb-cccc-1234567890ab", + "source_file_url": "https://files.example.com/exports/sessions.ndjson", + }, + status=201, + response_json={ + "uuid": "3f9c1a2b-4d5e-4f60-8a7b-9c0d1e2f3a4b", + "status": "PENDING", + "import_type": "user_verification", + "source_format": "ndjson", + "source_filename": "sessions.ndjson", + "total_rows": 0, + "processed_rows": 0, + "checkpoint_row": 0, + "created_count": 0, + "updated_count": 0, + "skipped_count": 0, + "failed_count": 0, + "total_media_bytes": 0, + "last_error": None, + "created_at": "2026-05-17T08:42:11Z", + "updated_at": "2026-05-17T08:42:11Z", + "started_at": None, + "completed_at": None, + }, + ), + lambda c: c.sessions.imports.create( + import_type="user_verification", + workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", + source_file_url="https://files.example.com/exports/sessions.ndjson", + ), + lambda c: c.sessions.imports.create( + import_type="user_verification", + workflow_id="9f9b1234-aaaa-bbbb-cccc-1234567890ab", + source_file_url="https://files.example.com/exports/sessions.ndjson", + ), + lambda r: isinstance(r, SessionImportJob), + lambda r: isinstance(r, SessionImportJob), + ) + + +def test_sessions_imports_list_errors() -> None: + """``GET /v3/session/imports/{importId}/errors/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/imports/00000000-0000-4000-8000-000000000001/errors/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "results": [ + { + "uuid": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d", + "row_number": 17, + "external_id": "ext-000017", + "error": "status: invalid value 'unknown_status'", + "raw_row": { + "external_id": "ext-000017", + "status": "unknown_status", + "first_name": "Jane", + }, + "created_at": "2026-05-17T08:44:02Z", + } + ], + }, + ), + lambda c: c.sessions.imports.list_errors( + "00000000-0000-4000-8000-000000000001", limit=1, offset=1 + ), + lambda c: c.sessions.imports.list_errors( + "00000000-0000-4000-8000-000000000001", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, SessionImportError) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, SessionImportError) for x1 in r.results) + ), + ) + + +def test_sessions_imports_retrieve() -> None: + """``GET /v3/session/imports/{importId}/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/imports/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "status": "PENDING", + "import_type": "user_verification", + "source_format": "csv", + "source_filename": "sample", + "total_rows": 1, + "processed_rows": 1, + "checkpoint_row": 1, + "created_count": 1, + "updated_count": 1, + "skipped_count": 1, + "failed_count": 1, + "total_media_bytes": 1, + "last_error": "sample", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + "started_at": "2026-01-01T00:00:00Z", + "completed_at": "2026-01-01T00:00:00Z", + }, + strict_type=SessionImportJob, + ), + lambda c: c.sessions.imports.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.sessions.imports.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, SessionImportJob), + lambda r: isinstance(r, SessionImportJob), + ) + + +def test_sessions_imports_retrieve_template() -> None: + """``GET /v3/session/imports/template/``""" + check_operation( + Expect( + method="GET", + path="/v3/session/imports/template/", + api="verification", + auth="api_key", + query={"import_type": ["user_verification"]}, + status=200, + response_bytes=BINARY_BODY, + response_content_type="text/csv", + ), + lambda c: c.sessions.imports.retrieve_template(import_type="user_verification"), + lambda c: c.sessions.imports.retrieve_template(import_type="user_verification"), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_sessions_reviews_create() -> None: + """``POST /v3/sessions/{session_id}/reviews/``""" + check_operation( + Expect( + method="POST", + path="/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/", + api="verification", + auth="api_key", + json_body={ + "new_status": "In Review", + "comment": "Flagging for manual document re-check.", + }, + status=201, + response_json={ + "user": "Unknown", + "new_status": "In Review", + "comment": "Flagging for manual document re-check.", + "created_at": "2026-06-12T00:18:44.466537Z", + }, + ), + lambda c: c.sessions.reviews.create( + "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d", + new_status="In Review", + comment="Flagging for manual document re-check.", + ), + lambda c: c.sessions.reviews.create( + "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d", + new_status="In Review", + comment="Flagging for manual document re-check.", + ), + lambda r: isinstance(r, SessionsReviewsCreateResponse), + lambda r: isinstance(r, SessionsReviewsCreateResponse), + ) + + +def test_sessions_reviews_list() -> None: + """``GET /v3/sessions/{session_id}/reviews/``""" + check_operation( + Expect( + method="GET", + path="/v3/sessions/8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d/reviews/", + api="verification", + auth="api_key", + query={"limit": ["50"], "offset": ["0"], "ordering": ["created_at"]}, + status=200, + response_json={ + "count": 3, + "next": None, + "previous": None, + "results": [ + { + "uuid": "de0561bc-5583-4409-9171-1a31a4c8a5f4", + "activity_type": "STATUS_UPDATED", + "actor_display": "API Client", + "actor_email": None, + "actor_type": "API_KEY", + "new_status": "Approved", + "previous_status": "In Review", + "comment": "Document re-checked manually; address matches.", + "mentioned_emails": [], + "previous_value": {"status": "In Review"}, + "new_value": {"status": "Approved"}, + "changed_fields": ["status"], + "metadata": None, + "created_at": "2026-06-12T00:19:31.918735Z", + }, + { + "uuid": "35370f58-5098-44ee-8a06-8c9af80b2455", + "activity_type": "STATUS_UPDATED", + "actor_display": "Unknown", + "actor_email": None, + "actor_type": "CONSOLE_USER", + "new_status": None, + "previous_status": None, + "comment": "Note without status.", + "mentioned_emails": [], + "previous_value": None, + "new_value": None, + "changed_fields": [], + "metadata": None, + "created_at": "2026-06-12T00:18:44.504157Z", + }, + { + "uuid": "00338f82-f1c5-4f57-8614-c49bb258af1a", + "activity_type": "STATUS_UPDATED", + "actor_display": "Unknown", + "actor_email": None, + "actor_type": "CONSOLE_USER", + "new_status": "In Review", + "previous_status": None, + "comment": "Flagging for manual document re-check.", + "mentioned_emails": [], + "previous_value": None, + "new_value": None, + "changed_fields": [], + "metadata": None, + "created_at": "2026-06-12T00:18:44.466537Z", + }, + ], + }, + ), + lambda c: c.sessions.reviews.list( + "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d", limit=50, offset=0, ordering="created_at" + ), + lambda c: c.sessions.reviews.list( + "8c2f3a14-7e9b-4d23-9e83-3f7d5e8a1c6d", limit=50, offset=0, ordering="created_at" + ), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, ReviewItem) for x1 in r.results), + lambda r: isinstance(r, AsyncPage) and all(isinstance(x1, ReviewItem) for x1 in r.results), + ) diff --git a/tests/generated/test_transactions.py b/tests/generated/test_transactions.py new file mode 100644 index 0000000..874b2c8 --- /dev/null +++ b/tests/generated/test_transactions.py @@ -0,0 +1,1230 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``transactions`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.transaction_detail import TransactionDetail +from didit.types.components.transaction_rule_backtest_result import TransactionRuleBacktestResult +from didit.types.components.transaction_rule_detail import TransactionRuleDetail +from didit.types.components.transaction_rule_install_result import TransactionRuleInstallResult +from didit.types.components.transaction_rule_library_entry import TransactionRuleLibraryEntry +from didit.types.components.travel_rule_transfer_detail import TravelRuleTransferDetail +from didit.types.transactions.create_sdk_token_response import TransactionsCreateSdkTokenResponse +from didit.types.transactions.list_networks_response import TransactionsListNetworksResponse +from didit.types.transactions.list_party_networks_response import ( + TransactionsListPartyNetworksResponse, +) +from didit.types.transactions.list_response import TransactionsListResponseResults +from tests.harness import Expect, check_operation + + +def test_transactions_create() -> None: + """``POST /v3/transactions/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/", + api="verification", + auth="api_key", + json_body={ + "transaction_id": "your-txn-2026-05-17-001", + "transaction_category": "finance", + "transaction_details": { + "direction": "outbound", + "amount": 1500, + "currency": "EUR", + "action_type": "withdrawal", + }, + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + }, + }, + status=201, + response_json={ + "uuid": "abcdef12-3456-4890-abcd-ef1234567890", + "environment": "live", + "transaction_number": 4123, + "txn_id": "your-txn-2026-05-17-001", + "txn_date": "2026-05-17T08:42:00Z", + "zone_id": "Europe/Madrid", + "transaction_type": "finance", + "action_type": "withdrawal", + "direction": "OUTBOUND", + "status": "APPROVED", + "source": "LIVE", + "amount": "1500", + "currency": "EUR", + "currency_type": "fiat", + "amount_in_default_currency": "1620.45", + "default_currency_code": "USD", + "preferred_currency_amount": "1620.45", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "payment_txn_id": None, + "score": 0, + "severity": None, + "decision_reason_code": None, + "decision_reason_label": None, + "vendor_data": "user-12345", + "metadata": { + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "address": {}, + "institution_details": {}, + "device_context": {}, + }, + "counterparty": {}, + }, + "props": {}, + "tags": [], + "parties": [ + { + "uuid": "b1111111-2222-4333-8444-555555555555", + "role": "APPLICANT", + "entity_type": "individual", + "kind": "USER", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "first_name": None, + "last_name": None, + "country_code": None, + "dob": None, + "address": {}, + "institution_info": {}, + "device": {}, + "external_party_snapshot": None, + } + ], + "payment_methods": [], + "documents": [], + "activities": [ + { + "uuid": "d4e5f6a7-8901-4bcd-9ef0-123456789abc", + "activity_type": "TRANSACTION_CREATED", + "source": "TRANSACTION", + "status": "APPROVED", + "title": "withdrawal", + "description": "Transaction recorded.", + "metadata": {}, + "occurred_at": "2026-05-17T08:42:11Z", + } + ], + "alerts": [], + "rule_runs": [], + "provider_results": [], + "travel_rule": None, + "network_snapshot": None, + "remediation": None, + "cost_breakdown": None, + }, + ), + lambda c: c.transactions.create( + transaction_id="your-txn-2026-05-17-001", + transaction_category="finance", + transaction_details={ + "direction": "outbound", + "amount": 1500, + "currency": "EUR", + "action_type": "withdrawal", + }, + subject={ + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + }, + ), + lambda c: c.transactions.create( + transaction_id="your-txn-2026-05-17-001", + transaction_category="finance", + transaction_details={ + "direction": "outbound", + "amount": 1500, + "currency": "EUR", + "action_type": "withdrawal", + }, + subject={ + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + }, + ), + lambda r: isinstance(r, TransactionDetail), + lambda r: isinstance(r, TransactionDetail), + ) + + +def test_transactions_create_sdk_token() -> None: + """``POST /v3/transactions/sdk-token/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/sdk-token/", + api="verification", + auth="api_key", + json_body={"vendor_data": "user-042", "ttl_seconds": 900, "max_uses": 3}, + status=201, + response_json={"sdk_token": "sample-token", "expires_at": "2026-07-07T12:15:00Z"}, + ), + lambda c: c.transactions.create_sdk_token( + vendor_data="user-042", ttl_seconds=900, max_uses=3 + ), + lambda c: c.transactions.create_sdk_token( + vendor_data="user-042", ttl_seconds=900, max_uses=3 + ), + lambda r: isinstance(r, TransactionsCreateSdkTokenResponse), + lambda r: isinstance(r, TransactionsCreateSdkTokenResponse), + ) + + +def test_transactions_list() -> None: + """``GET /v3/transactions/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": None, + "previous": None, + "total_transactions": 1, + "results": [ + { + "uuid": "abcdef12-3456-4890-abcd-ef1234567890", + "transaction_number": 4123, + "txn_id": "your-txn-2026-05-17-001", + "transaction_type": "finance", + "action_type": "withdrawal", + "status": "IN_REVIEW", + "direction": "OUTBOUND", + "amount": "1500", + "currency": "EUR", + "preferred_currency_amount": "1620.45", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "txn_date": "2026-05-17T08:42:00Z", + "created_at": "2026-05-17T08:42:11Z", + "score": 78, + "severity": "HIGH", + "decision_reason_code": "WALLET_HIGH_RISK", + "decision_reason_label": "Destination wallet flagged as high risk", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "Manual review", + "color": "#F59E0B", + "description": None, + "source": "custom", + } + ], + "subject_name": "Maria Garcia", + "subject_vendor_data": "user-12345", + "counterparty_name": None, + "counterparty_vendor_data": None, + "counterparty_country": None, + "subject_country": None, + "assigned_to_name": "Alex Reviewer", + } + ], + }, + ), + lambda c: c.transactions.list(limit=1, offset=1), + lambda c: c.transactions.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, TransactionsListResponseResults) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, TransactionsListResponseResults) for x1 in r.results) + ), + ) + + +def test_transactions_list_networks() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/transactions/00000000-0000-4000-8000-000000000001/networks/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "network": { + "uuid": "00000000-0000-4000-8000-000000000007", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + }, + "subject": {}, + "signal_types": ["ip_address"], + "signal_count": 1, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=TransactionsListNetworksResponse, + ), + lambda c: c.transactions.list_networks("00000000-0000-4000-8000-000000000001"), + lambda c: c.transactions.list_networks("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, TransactionsListNetworksResponse), + lambda r: isinstance(r, TransactionsListNetworksResponse), + ) + + +def test_transactions_list_party_networks() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/transactions/{transaction_uuid}/parties/{transaction_party_uuid}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/transactions/00000000-0000-4000-8000-000000000001/parties/00000000-0000-4000-8000-000000000002/networks/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "network": { + "uuid": "00000000-0000-4000-8000-000000000007", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + }, + "subject": {}, + "signal_types": ["ip_address"], + "signal_count": 1, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=TransactionsListPartyNetworksResponse, + ), + lambda c: c.transactions.list_party_networks( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda c: c.transactions.list_party_networks( + "00000000-0000-4000-8000-000000000001", "00000000-0000-4000-8000-000000000002" + ), + lambda r: isinstance(r, TransactionsListPartyNetworksResponse), + lambda r: isinstance(r, TransactionsListPartyNetworksResponse), + ) + + +def test_transactions_retrieve() -> None: + """``GET /v3/transactions/{transaction_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/abcdef12-3456-7890-abcd-ef1234567890/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "abcdef12-3456-4890-abcd-ef1234567890", + "environment": "live", + "transaction_number": 4123, + "txn_id": "your-txn-2026-05-17-001", + "txn_date": "2026-05-17T08:42:00Z", + "zone_id": "Europe/Madrid", + "transaction_type": "finance", + "action_type": "withdrawal", + "direction": "OUTBOUND", + "status": "IN_REVIEW", + "source": "LIVE", + "amount": "1500", + "currency": "EUR", + "currency_type": "fiat", + "amount_in_default_currency": "1620.45", + "default_currency_code": "USD", + "preferred_currency_amount": "1620.45", + "preferred_currency_code": "USD", + "payment_details": "Withdrawal to crypto wallet", + "payment_txn_id": None, + "score": 78, + "severity": "HIGH", + "decision_reason_code": "WALLET_HIGH_RISK", + "decision_reason_label": "Destination wallet flagged as high risk", + "vendor_data": "user-12345", + "metadata": { + "subject": { + "entity_type": "individual", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "address": {}, + "institution_details": {}, + "device_context": {}, + }, + "counterparty": {}, + }, + "props": {"order_id": "ord-9988"}, + "tags": [], + "parties": [ + { + "uuid": "b1111111-2222-4333-8444-555555555555", + "role": "APPLICANT", + "entity_type": "individual", + "kind": "USER", + "vendor_data": "user-12345", + "full_name": "Maria Garcia", + "first_name": None, + "last_name": None, + "country_code": None, + "dob": None, + "address": {}, + "institution_info": {}, + "device": {}, + "external_party_snapshot": None, + } + ], + "payment_methods": [], + "documents": [], + "activities": [], + "alerts": [ + { + "uuid": "a7b8c9d0-1234-4abc-9def-567890abcdef", + "title": "Destination wallet flagged as high risk", + "description": "Provider risk score exceeded the configured threshold.", + "severity": "HIGH", + "status": "OPEN", + "source": "RULE", + "metadata": {}, + "created_at": "2026-05-17T08:42:12Z", + "due_at": None, + } + ], + "rule_runs": [], + "provider_results": [], + "travel_rule": None, + "network_snapshot": None, + "remediation": None, + "cost_breakdown": None, + }, + ), + lambda c: c.transactions.retrieve("abcdef12-3456-7890-abcd-ef1234567890"), + lambda c: c.transactions.retrieve("abcdef12-3456-7890-abcd-ef1234567890"), + lambda r: isinstance(r, TransactionDetail), + lambda r: isinstance(r, TransactionDetail), + ) + + +def test_transactions_rules_backtest() -> None: + """``POST /v3/transactions/rules/backtest/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/rules/backtest/", + api="verification", + auth="api_key", + json_body={ + "conditions": [ + {"field": "currency", "operator": "eq", "value": "EUR"}, + {"field": "amount", "operator": "lt", "value": 10000}, + ], + "aggregation": [ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + ], + "evaluation_mode": "ALL", + "scope": {"transaction_types": ["finance"], "directions": ["INBOUND"]}, + "period_days": 90, + }, + status=200, + response_json={ + "evaluated": 4820, + "matched": 12, + "affected_entities": 4, + "period_days": 90, + }, + ), + lambda c: c.transactions.rules.backtest( + conditions=[ + {"field": "currency", "operator": "eq", "value": "EUR"}, + {"field": "amount", "operator": "lt", "value": 10000}, + ], + aggregation=[ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + ], + evaluation_mode="ALL", + scope={"transaction_types": ["finance"], "directions": ["INBOUND"]}, + period_days=90, + ), + lambda c: c.transactions.rules.backtest( + conditions=[ + {"field": "currency", "operator": "eq", "value": "EUR"}, + {"field": "amount", "operator": "lt", "value": 10000}, + ], + aggregation=[ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + ], + evaluation_mode="ALL", + scope={"transaction_types": ["finance"], "directions": ["INBOUND"]}, + period_days=90, + ), + lambda r: isinstance(r, TransactionRuleBacktestResult), + lambda r: isinstance(r, TransactionRuleBacktestResult), + ) + + +def test_transactions_rules_create() -> None: + """``POST /v3/transactions/rules/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/rules/", + api="verification", + auth="api_key", + json_body={ + "title": "Structuring - inbound (custom)", + "category": "finance", + "mode": "TEST", + "description": "20+ inbound EUR transfers, all under 10,000 EUR, 30 days", + "evaluation_mode": "ALL", + "scope": {"transaction_types": ["finance"], "directions": ["INBOUND"]}, + "conditions": [ + {"field": "currency", "operator": "eq", "value": "EUR"}, + {"field": "amount", "operator": "lt", "value": 10000}, + ], + "aggregation": [ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + ], + "actions": [{"type": "add_score", "value": 35}], + }, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "title": "sample", + "description": "sample", + "category": "sample", + "mode": "ACTIVE", + "evaluation_mode": "ALL", + "scope": { + "transaction_types": ["sample"], + "directions": ["INBOUND"], + "action_types": ["sample"], + }, + "conditions": [ + { + "field": "sample", + "operator": "eq", + "value": "value", + "value_type": "list", + "score": 1.5, + "group_index": 1, + "group_logic": "AND", + "groups_logic": "AND", + } + ], + "aggregation": [ + { + "metric": "count", + "field": "sample", + "operator": "eq", + "value": 1.5, + "window": "sample", + "filters": {}, + } + ], + "actions": [{"type": "add_score", "value": 1}], + "metadata": {}, + "severity": "UNKNOWN", + "source": "PRESET", + "library_key": "sample", + "latest_triggered_at": "2026-01-01T00:00:00Z", + "run_count": 1, + "approved_count": 1, + "reviewed_count": 1, + "declined_count": 1, + "awaiting_user_count": 1, + "approved_pct": 1.5, + "reviewed_pct": 1.5, + "declined_pct": 1.5, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=TransactionRuleDetail, + ), + lambda c: c.transactions.rules.create( + title="Structuring - inbound (custom)", + category="finance", + mode="TEST", + description="20+ inbound EUR transfers, all under 10,000 EUR, 30 days", + evaluation_mode="ALL", + scope={"transaction_types": ["finance"], "directions": ["INBOUND"]}, + conditions=[ + {"field": "currency", "operator": "eq", "value": "EUR"}, + {"field": "amount", "operator": "lt", "value": 10000}, + ], + aggregation=[ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + ], + actions=[{"type": "add_score", "value": 35}], + ), + lambda c: c.transactions.rules.create( + title="Structuring - inbound (custom)", + category="finance", + mode="TEST", + description="20+ inbound EUR transfers, all under 10,000 EUR, 30 days", + evaluation_mode="ALL", + scope={"transaction_types": ["finance"], "directions": ["INBOUND"]}, + conditions=[ + {"field": "currency", "operator": "eq", "value": "EUR"}, + {"field": "amount", "operator": "lt", "value": 10000}, + ], + aggregation=[ + { + "metric": "count", + "operator": "gte", + "value": 20, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + { + "metric": "max", + "field": "amount", + "operator": "lt", + "value": 10000, + "window": "30d", + "filters": { + "direction": "INBOUND", + "transaction_type": "finance", + "subject_vendor_data": "__current__", + "currency": "EUR", + }, + }, + ], + actions=[{"type": "add_score", "value": 35}], + ), + lambda r: isinstance(r, TransactionRuleDetail), + lambda r: isinstance(r, TransactionRuleDetail), + ) + + +def test_transactions_rules_delete() -> None: + """``DELETE /v3/transactions/rules/{rule_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/transactions/rules/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.transactions.rules.delete("00000000-0000-4000-8000-000000000001"), + lambda c: c.transactions.rules.delete("00000000-0000-4000-8000-000000000001"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_transactions_rules_install_library() -> None: + """``POST /v3/transactions/rules/install/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/rules/install/", + api="verification", + auth="api_key", + json_body={"library_keys": ["structuring-inbound", "structuring-outbound"]}, + status=200, + response_json={ + "installed_library_keys": ["structuring-inbound", "structuring-outbound"], + "installed_count": 2, + }, + ), + lambda c: c.transactions.rules.install_library( + library_keys=["structuring-inbound", "structuring-outbound"] + ), + lambda c: c.transactions.rules.install_library( + library_keys=["structuring-inbound", "structuring-outbound"] + ), + lambda r: isinstance(r, TransactionRuleInstallResult), + lambda r: isinstance(r, TransactionRuleInstallResult), + ) + + +def test_transactions_rules_list() -> None: + """``GET /v3/transactions/rules/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/rules/", + api="verification", + auth="api_key", + query={ + "source": ["PRESET"], + "category": ["sample"], + "mode": ["ACTIVE"], + "bundle": ["sample"], + "search": ["sample"], + "ordering": ["sample"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "title": "sample", + "description": "sample", + "category": "sample", + "mode": "ACTIVE", + "evaluation_mode": "ALL", + "scope": { + "transaction_types": ["sample"], + "directions": ["INBOUND"], + "action_types": ["sample"], + }, + "conditions": [ + { + "field": "sample", + "operator": "eq", + "value": "value", + "value_type": "list", + "score": 1.5, + "group_index": 1, + "group_logic": "AND", + "groups_logic": "AND", + } + ], + "aggregation": [ + { + "metric": "count", + "field": "sample", + "operator": "eq", + "value": 1.5, + "window": "sample", + "filters": {}, + } + ], + "actions": [{"type": "add_score", "value": 1}], + "metadata": {}, + "severity": "UNKNOWN", + "source": "PRESET", + "library_key": "sample", + "latest_triggered_at": "2026-01-01T00:00:00Z", + "run_count": 1, + "approved_count": 1, + "reviewed_count": 1, + "declined_count": 1, + "awaiting_user_count": 1, + "approved_pct": 1.5, + "reviewed_pct": 1.5, + "declined_pct": 1.5, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.transactions.rules.list( + source="PRESET", + category="sample", + mode="ACTIVE", + bundle="sample", + search="sample", + ordering="sample", + limit=1, + offset=1, + ), + lambda c: c.transactions.rules.list( + source="PRESET", + category="sample", + mode="ACTIVE", + bundle="sample", + search="sample", + ordering="sample", + limit=1, + offset=1, + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, TransactionRuleDetail) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, TransactionRuleDetail) for x1 in r.results) + ), + ) + + +def test_transactions_rules_list_library() -> None: + """``GET /v3/transactions/rules/library/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/rules/library/", + api="verification", + auth="api_key", + query={ + "bundle": ["sample"], + "category": ["sample"], + "search": ["sample"], + "limit": ["1"], + "offset": ["1"], + }, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "library_key": "sample", + "title": "sample", + "description": "sample", + "category": "sample", + "evaluation_mode": "ALL", + "scope": { + "transaction_types": ["sample"], + "directions": ["INBOUND"], + "action_types": ["sample"], + }, + "conditions": [ + { + "field": "sample", + "operator": "eq", + "value": "value", + "value_type": "list", + "score": 1.5, + "group_index": 1, + "group_logic": "AND", + "groups_logic": "AND", + } + ], + "aggregation": [ + { + "metric": "count", + "field": "sample", + "operator": "eq", + "value": 1.5, + "window": "sample", + "filters": {}, + } + ], + "actions": [{"type": "add_score", "value": 1}], + "metadata": {}, + "bundle": "sample", + "tags": ["sample"], + "industries": ["sample"], + "source": "sample", + "is_installed": True, + } + ], + }, + ), + lambda c: c.transactions.rules.list_library( + bundle="sample", category="sample", search="sample", limit=1, offset=1 + ), + lambda c: c.transactions.rules.list_library( + bundle="sample", category="sample", search="sample", limit=1, offset=1 + ), + lambda r: ( + isinstance(r, SyncPage) + and all(isinstance(x1, TransactionRuleLibraryEntry) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) + and all(isinstance(x1, TransactionRuleLibraryEntry) for x1 in r.results) + ), + ) + + +def test_transactions_rules_retrieve() -> None: + """``GET /v3/transactions/rules/{rule_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/transactions/rules/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "title": "sample", + "description": "sample", + "category": "sample", + "mode": "ACTIVE", + "evaluation_mode": "ALL", + "scope": { + "transaction_types": ["sample"], + "directions": ["INBOUND"], + "action_types": ["sample"], + }, + "conditions": [ + { + "field": "sample", + "operator": "eq", + "value": "value", + "value_type": "list", + "score": 1.5, + "group_index": 1, + "group_logic": "AND", + "groups_logic": "AND", + } + ], + "aggregation": [ + { + "metric": "count", + "field": "sample", + "operator": "eq", + "value": 1.5, + "window": "sample", + "filters": {}, + } + ], + "actions": [{"type": "add_score", "value": 1}], + "metadata": {}, + "severity": "UNKNOWN", + "source": "PRESET", + "library_key": "sample", + "latest_triggered_at": "2026-01-01T00:00:00Z", + "run_count": 1, + "approved_count": 1, + "reviewed_count": 1, + "declined_count": 1, + "awaiting_user_count": 1, + "approved_pct": 1.5, + "reviewed_pct": 1.5, + "declined_pct": 1.5, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=TransactionRuleDetail, + ), + lambda c: c.transactions.rules.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.transactions.rules.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, TransactionRuleDetail), + lambda r: isinstance(r, TransactionRuleDetail), + ) + + +def test_transactions_rules_uninstall_library() -> None: + """``DELETE /v3/transactions/rules/install/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/transactions/rules/install/", + api="verification", + auth="api_key", + json_body={"bundle": "responsible_gaming"}, + status=200, + response_json={"uninstalled_count": 5}, + ), + lambda c: c.transactions.rules.uninstall_library(bundle="responsible_gaming"), + lambda c: c.transactions.rules.uninstall_library(bundle="responsible_gaming"), + lambda r: isinstance(r, TransactionRuleInstallResult), + lambda r: isinstance(r, TransactionRuleInstallResult), + ) + + +def test_transactions_rules_update() -> None: + """``PATCH /v3/transactions/rules/{rule_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/transactions/rules/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"mode": "ACTIVE"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "title": "sample", + "description": "sample", + "category": "sample", + "mode": "ACTIVE", + "evaluation_mode": "ALL", + "scope": { + "transaction_types": ["sample"], + "directions": ["INBOUND"], + "action_types": ["sample"], + }, + "conditions": [ + { + "field": "sample", + "operator": "eq", + "value": "value", + "value_type": "list", + "score": 1.5, + "group_index": 1, + "group_logic": "AND", + "groups_logic": "AND", + } + ], + "aggregation": [ + { + "metric": "count", + "field": "sample", + "operator": "eq", + "value": 1.5, + "window": "sample", + "filters": {}, + } + ], + "actions": [{"type": "add_score", "value": 1}], + "metadata": {}, + "severity": "UNKNOWN", + "source": "PRESET", + "library_key": "sample", + "latest_triggered_at": "2026-01-01T00:00:00Z", + "run_count": 1, + "approved_count": 1, + "reviewed_count": 1, + "declined_count": 1, + "awaiting_user_count": 1, + "approved_pct": 1.5, + "reviewed_pct": 1.5, + "declined_pct": 1.5, + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z", + }, + strict_type=TransactionRuleDetail, + ), + lambda c: c.transactions.rules.update( + "00000000-0000-4000-8000-000000000001", mode="ACTIVE" + ), + lambda c: c.transactions.rules.update( + "00000000-0000-4000-8000-000000000001", mode="ACTIVE" + ), + lambda r: isinstance(r, TransactionRuleDetail), + lambda r: isinstance(r, TransactionRuleDetail), + ) + + +def test_transactions_travel_rule_confirm_ownership() -> None: + """``POST /v3/transactions/{transaction_id}/travel-rule/ownership/``""" + check_operation( + Expect( + method="POST", + path="/v3/transactions/00000000-0000-4000-8000-000000000001/travel-rule/ownership/", + api="verification", + auth="api_key", + json_body={"confirmed": True}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "status": "AWAITING_COUNTERPARTY", + "direction": "INBOUND", + "rail": "INTERNAL", + "protocol": "sample", + "ivms_version": "ivms101.2023", + "required": True, + "ownership_confirmed": True, + "deadline_at": "2026-01-01T00:00:00Z", + "timeline": [ + {"status": "sample", "at": "2026-01-01T00:00:00Z", "detail": "sample"} + ], + "counterparty_vasp": {"name": "sample", "lei": "sample", "dd_score": 1}, + "originator_data": {}, + "beneficiary_data": {}, + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=TravelRuleTransferDetail, + ), + lambda c: c.transactions.travel_rule.confirm_ownership( + "00000000-0000-4000-8000-000000000001", confirmed=True + ), + lambda c: c.transactions.travel_rule.confirm_ownership( + "00000000-0000-4000-8000-000000000001", confirmed=True + ), + lambda r: isinstance(r, TravelRuleTransferDetail), + lambda r: isinstance(r, TravelRuleTransferDetail), + ) + + +def test_transactions_travel_rule_update_transfer() -> None: + """``PATCH /v3/transactions/{transaction_id}/travel-rule/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/transactions/00000000-0000-4000-8000-000000000001/travel-rule/", + api="verification", + auth="api_key", + json_body={"payment_txn_id": "0xchainhash"}, + status=200, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "status": "AWAITING_COUNTERPARTY", + "direction": "INBOUND", + "rail": "INTERNAL", + "protocol": "sample", + "ivms_version": "ivms101.2023", + "required": True, + "ownership_confirmed": True, + "deadline_at": "2026-01-01T00:00:00Z", + "timeline": [ + {"status": "sample", "at": "2026-01-01T00:00:00Z", "detail": "sample"} + ], + "counterparty_vasp": {"name": "sample", "lei": "sample", "dd_score": 1}, + "originator_data": {}, + "beneficiary_data": {}, + "created_at": "2026-01-01T00:00:00Z", + }, + strict_type=TravelRuleTransferDetail, + ), + lambda c: c.transactions.travel_rule.update_transfer( + "00000000-0000-4000-8000-000000000001", payment_txn_id="0xchainhash" + ), + lambda c: c.transactions.travel_rule.update_transfer( + "00000000-0000-4000-8000-000000000001", payment_txn_id="0xchainhash" + ), + lambda r: isinstance(r, TravelRuleTransferDetail), + lambda r: isinstance(r, TravelRuleTransferDetail), + ) diff --git a/tests/generated/test_travel_rule.py b/tests/generated/test_travel_rule.py new file mode 100644 index 0000000..886f3db --- /dev/null +++ b/tests/generated/test_travel_rule.py @@ -0,0 +1,458 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``travel_rule`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.travel_rule_inbound_response import TravelRuleInboundResponse +from didit.types.components.travel_rule_settings_detail import TravelRuleSettingsDetail +from didit.types.components.travel_rule_widget_session_response import ( + TravelRuleWidgetSessionResponse, +) +from didit.types.components.vasp_directory_entry import VaspDirectoryEntry +from didit.types.components.wallet_address_entry import WalletAddressEntry +from didit.types.components.wallet_address_entry_update import WalletAddressEntryUpdate +from tests.harness import Expect, check_operation + + +def test_travel_rule_create_widget_session() -> None: + """``POST /v3/travel-rule/widget-session/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/widget-session/", + api="verification", + auth="api_key", + json_body={ + "wallet_address": "0xBeneficiaryWallet01", + "chain": "ethereum", + "holder_name": "Ana Diaz", + "vendor_data": "user-042", + "transaction_id": "22222222-3333-4444-5555-666666666666", + "expires_in_minutes": 60, + }, + status=201, + response_json={ + "widget_session_id": "00000000-0000-4000-8000-000000000003", + "token": "sample-token", + "url": "sample", + "expires_at": "2026-01-01T00:00:00Z", + }, + strict_type=TravelRuleWidgetSessionResponse, + ), + lambda c: c.travel_rule.create_widget_session( + wallet_address="0xBeneficiaryWallet01", + chain="ethereum", + holder_name="Ana Diaz", + vendor_data="user-042", + transaction_id="22222222-3333-4444-5555-666666666666", + expires_in_minutes=60, + ), + lambda c: c.travel_rule.create_widget_session( + wallet_address="0xBeneficiaryWallet01", + chain="ethereum", + holder_name="Ana Diaz", + vendor_data="user-042", + transaction_id="22222222-3333-4444-5555-666666666666", + expires_in_minutes=60, + ), + lambda r: isinstance(r, TravelRuleWidgetSessionResponse), + lambda r: isinstance(r, TravelRuleWidgetSessionResponse), + ) + + +def test_travel_rule_list_vasps() -> None: + """``GET /v3/travel-rule/vasps/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/vasps/", + api="verification", + auth="api_key", + query={"search": ["sample"], "limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "name": "sample", + "lei": "sample", + "jurisdiction": "sample", + "dd_status": "NOT_STARTED", + "dd_score": 1, + "reachable_rails": ["INTERNAL"], + "is_didit_customer": True, + "dd_score_breakdown": {"key": 1}, + "dd_assessed_at": "2026-01-01T00:00:00Z", + } + ], + }, + ), + lambda c: c.travel_rule.list_vasps(search="sample", limit=1, offset=1), + lambda c: c.travel_rule.list_vasps(search="sample", limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, VaspDirectoryEntry) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, VaspDirectoryEntry) for x1 in r.results) + ), + ) + + +def test_travel_rule_register_inbound() -> None: + """``POST /v3/travel-rule/inbound/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/inbound/", + api="verification", + auth="api_key", + json_body={ + "chain": "ethereum", + "txid": "0xdeposit-hash", + "wallet_address": "0xYourDepositWallet", + "amount": "500.00", + "currency": "USDC", + "originator_data": {"name": "Origin CASP SL"}, + "beneficiary_data": {"name": "Ana Diaz"}, + "originating_vasp": { + "name": "Origin CASP SL", + "lei": "", + "travel_address": "ta...", + }, + }, + status=200, + response_json={ + "transaction_id": "00000000-0000-4000-8000-000000000003", + "txn_id": "sample", + "created": True, + "travel_rule": { + "uuid": "00000000-0000-4000-8000-000000000005", + "status": "AWAITING_COUNTERPARTY", + "direction": "INBOUND", + "rail": "INTERNAL", + "protocol": "sample", + "ivms_version": "ivms101.2023", + "required": True, + "ownership_confirmed": True, + "deadline_at": "2026-01-01T00:00:00Z", + "timeline": [ + {"status": "sample", "at": "2026-01-01T00:00:00Z", "detail": "sample"} + ], + "counterparty_vasp": {"name": "sample", "lei": "sample", "dd_score": 1}, + "originator_data": {}, + "beneficiary_data": {}, + "created_at": "2026-01-01T00:00:00Z", + }, + }, + strict_type=TravelRuleInboundResponse, + ), + lambda c: c.travel_rule.register_inbound( + chain="ethereum", + txid="0xdeposit-hash", + wallet_address="0xYourDepositWallet", + amount="500.00", + currency="USDC", + originator_data={"name": "Origin CASP SL"}, + beneficiary_data={"name": "Ana Diaz"}, + originating_vasp={"name": "Origin CASP SL", "lei": "", "travel_address": "ta..."}, + ), + lambda c: c.travel_rule.register_inbound( + chain="ethereum", + txid="0xdeposit-hash", + wallet_address="0xYourDepositWallet", + amount="500.00", + currency="USDC", + originator_data={"name": "Origin CASP SL"}, + beneficiary_data={"name": "Ana Diaz"}, + originating_vasp={"name": "Origin CASP SL", "lei": "", "travel_address": "ta..."}, + ), + lambda r: isinstance(r, TravelRuleInboundResponse), + lambda r: isinstance(r, TravelRuleInboundResponse), + ) + + +def test_travel_rule_retrieve_settings() -> None: + """``GET /v3/travel-rule/settings/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/settings/", + api="verification", + auth="api_key", + status=200, + response_json={ + "is_enabled": False, + "jurisdiction": "EU", + "name_matching_strictness": "DEFAULT", + "confirmation_timeout_hours": 48, + "timeout_outcome": "HOLD", + "threshold_amount": "0.00", + "legal_name": "", + "lei": "", + "compliance_email": "", + "is_discoverable": True, + "disabled_networks": [], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "unavailable", + "platform_available": False, + "connected": False, + }, + "trust": { + "rail": "TRUST", + "mode": "unavailable", + "platform_available": False, + "connected": False, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "unavailable", + "platform_available": False, + "connected": False, + }, + "sygna": { + "rail": "SYGNA", + "mode": "unavailable", + "platform_available": False, + "connected": False, + }, + }, + }, + ), + lambda c: c.travel_rule.retrieve_settings(), + lambda c: c.travel_rule.retrieve_settings(), + lambda r: isinstance(r, TravelRuleSettingsDetail), + lambda r: isinstance(r, TravelRuleSettingsDetail), + ) + + +def test_travel_rule_update_settings() -> None: + """``PUT /v3/travel-rule/settings/``""" + check_operation( + Expect( + method="PUT", + path="/v3/travel-rule/settings/", + api="verification", + auth="api_key", + json_body={ + "is_enabled": True, + "legal_name": "Origin CASP SL", + "jurisdiction": "EU", + "name_matching_strictness": "DEFAULT", + "confirmation_timeout_hours": 48, + "timeout_outcome": "HOLD", + "is_discoverable": True, + }, + status=200, + response_json={ + "is_enabled": True, + "jurisdiction": "sample", + "name_matching_strictness": "NONE", + "confirmation_timeout_hours": 1, + "timeout_outcome": "HOLD", + "threshold_amount": "sample", + "legal_name": "sample", + "lei": "sample", + "compliance_email": "user@example.com", + "is_discoverable": True, + "inbound_auto_accept": True, + "allow_self_declaration": True, + "allow_screenshot_proof": True, + "auto_wallet_verification": True, + "vasp_attribution_enabled": True, + "travel_address": "sample", + "disabled_networks": ["GTR"], + "networks": { + "gtr": { + "rail": "GTR", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "trust": { + "rail": "TRUST", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "verifyvasp": { + "rail": "VERIFYVASP", + "mode": "own", + "platform_available": True, + "connected": True, + }, + "sygna": { + "rail": "SYGNA", + "mode": "own", + "platform_available": True, + "connected": True, + }, + }, + }, + strict_type=TravelRuleSettingsDetail, + ), + lambda c: c.travel_rule.update_settings( + is_enabled=True, + legal_name="Origin CASP SL", + jurisdiction="EU", + name_matching_strictness="DEFAULT", + confirmation_timeout_hours=48, + timeout_outcome="HOLD", + is_discoverable=True, + ), + lambda c: c.travel_rule.update_settings( + is_enabled=True, + legal_name="Origin CASP SL", + jurisdiction="EU", + name_matching_strictness="DEFAULT", + confirmation_timeout_hours=48, + timeout_outcome="HOLD", + is_discoverable=True, + ), + lambda r: isinstance(r, TravelRuleSettingsDetail), + lambda r: isinstance(r, TravelRuleSettingsDetail), + ) + + +def test_travel_rule_wallet_addresses_create() -> None: + """``POST /v3/travel-rule/wallet-addresses/``""" + check_operation( + Expect( + method="POST", + path="/v3/travel-rule/wallet-addresses/", + api="verification", + auth="api_key", + json_body={ + "address": "0xBeneficiaryWallet01", + "chain": "ethereum", + "holder_vendor_data": "user-042", + "holder_name": "Ana Diaz", + "self_declared": True, + }, + status=201, + response_json={ + "uuid": "00000000-0000-4000-8000-000000000003", + "address": "sample", + "chain": "sample", + "holder_vendor_data": "sample", + "holder_name": "sample", + "entity_type": "sample", + "is_ownership_verified": True, + "created_at": "2026-01-01T00:00:00Z", + "self_declared": True, + "travel_address": "sample", + }, + strict_type=WalletAddressEntry, + ), + lambda c: c.travel_rule.wallet_addresses.create( + address="0xBeneficiaryWallet01", + chain="ethereum", + holder_vendor_data="user-042", + holder_name="Ana Diaz", + self_declared=True, + ), + lambda c: c.travel_rule.wallet_addresses.create( + address="0xBeneficiaryWallet01", + chain="ethereum", + holder_vendor_data="user-042", + holder_name="Ana Diaz", + self_declared=True, + ), + lambda r: isinstance(r, WalletAddressEntry), + lambda r: isinstance(r, WalletAddressEntry), + ) + + +def test_travel_rule_wallet_addresses_delete() -> None: + """``DELETE /v3/travel-rule/wallet-addresses/{entry_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/travel-rule/wallet-addresses/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.travel_rule.wallet_addresses.delete("00000000-0000-4000-8000-000000000001"), + lambda c: c.travel_rule.wallet_addresses.delete("00000000-0000-4000-8000-000000000001"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_travel_rule_wallet_addresses_list() -> None: + """``GET /v3/travel-rule/wallet-addresses/``""" + check_operation( + Expect( + method="GET", + path="/v3/travel-rule/wallet-addresses/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 1, + "next": "sample", + "previous": "sample", + "results": [ + { + "uuid": "00000000-0000-4000-8000-000000000005", + "address": "sample", + "chain": "sample", + "holder_vendor_data": "sample", + "holder_name": "sample", + "entity_type": "sample", + "is_ownership_verified": True, + "created_at": "2026-01-01T00:00:00Z", + "self_declared": True, + "travel_address": "sample", + } + ], + }, + ), + lambda c: c.travel_rule.wallet_addresses.list(limit=1, offset=1), + lambda c: c.travel_rule.wallet_addresses.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, WalletAddressEntry) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, WalletAddressEntry) for x1 in r.results) + ), + ) + + +def test_travel_rule_wallet_addresses_update() -> None: + """``PATCH /v3/travel-rule/wallet-addresses/{entry_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/travel-rule/wallet-addresses/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"holder_name": "Ana Updated", "travel_address": "ta..."}, + status=200, + response_json={ + "holder_name": "sample", + "holder_vendor_data": "sample", + "entity_type": "sample", + "travel_address": "sample", + }, + strict_type=WalletAddressEntryUpdate, + ), + lambda c: c.travel_rule.wallet_addresses.update( + "00000000-0000-4000-8000-000000000001", + holder_name="Ana Updated", + travel_address="ta...", + ), + lambda c: c.travel_rule.wallet_addresses.update( + "00000000-0000-4000-8000-000000000001", + holder_name="Ana Updated", + travel_address="ta...", + ), + lambda r: isinstance(r, WalletAddressEntryUpdate), + lambda r: isinstance(r, WalletAddressEntryUpdate), + ) diff --git a/tests/generated/test_users.py b/tests/generated/test_users.py new file mode 100644 index 0000000..0d4595e --- /dev/null +++ b/tests/generated/test_users.py @@ -0,0 +1,653 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``users`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.user_detail_item import UserDetailItem +from didit.types.components.user_list_item import UserListItem +from didit.types.users.batch_delete_response import UsersBatchDeleteResponse +from didit.types.users.list_networks_response import UsersListNetworksResponse +from didit.types.users.retrieve_ongoing_monitoring_job_response import ( + UsersRetrieveOngoingMonitoringJobResponse, +) +from didit.types.users.set_aml_monitoring_response import ( + UsersSetAmlMonitoringResponse202, + UsersSetAmlMonitoringResponseVariant1, + UsersSetAmlMonitoringResponseVariant2, +) +from didit.types.users.set_ongoing_monitoring_response import ( + UsersSetOngoingMonitoringResponse202, + UsersSetOngoingMonitoringResponseVariant1, + UsersSetOngoingMonitoringResponseVariant2, +) +from didit.types.users.upload_face_response import UsersUploadFaceResponse +from tests.harness import BINARY_BODY, Expect, check_operation + + +def test_users_batch_delete() -> None: + """``POST /v3/users/delete/``""" + check_operation( + Expect( + method="POST", + path="/v3/users/delete/", + api="verification", + auth="api_key", + json_body={"vendor_data_list": ["user-123", "user-456"]}, + status=200, + response_json={"deleted": 2}, + ), + lambda c: c.users.batch_delete(vendor_data_list=["user-123", "user-456"]), + lambda c: c.users.batch_delete(vendor_data_list=["user-123", "user-456"]), + lambda r: isinstance(r, UsersBatchDeleteResponse), + lambda r: isinstance(r, UsersBatchDeleteResponse), + ) + + +def test_users_create() -> None: + """``POST /v3/users/create/``""" + check_operation( + Expect( + method="POST", + path="/v3/users/create/", + api="verification", + auth="api_key", + json_body={"vendor_data": "user-abc-123", "full_name": "John Doe"}, + status=201, + response_json={ + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": None, + "full_name": "John Doe", + "date_of_birth": None, + "effective_name": "John Doe", + "status": "ACTIVE", + "metadata": {}, + "portrait_image_url": None, + "session_count": 0, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 0, + "issuing_states": [], + "approved_emails": [], + "approved_phones": [], + "features": {}, + "features_list": [], + "last_session_at": None, + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": None, + "tags": [], + "comments": [], + "created_at": "2025-06-15T10:30:00Z", + "updated_at": "2025-06-15T10:30:00Z", + }, + ), + lambda c: c.users.create(vendor_data="user-abc-123", full_name="John Doe"), + lambda c: c.users.create(vendor_data="user-abc-123", full_name="John Doe"), + lambda r: isinstance(r, UserDetailItem), + lambda r: isinstance(r, UserDetailItem), + ) + + +def test_users_generate_pdf() -> None: + """``GET /v3/users/{vendor_data}/generate-pdf/``""" + check_operation( + Expect( + method="GET", + path="/v3/users/user-abc-123/generate-pdf/", + api="verification", + auth="api_key", + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.users.generate_pdf("user-abc-123"), + lambda c: c.users.generate_pdf("user-abc-123"), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) + + +def test_users_list() -> None: + """``GET /v3/users/``""" + check_operation( + Expect( + method="GET", + path="/v3/users/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": None, + "full_name": "John Michael Doe", + "date_of_birth": "1990-05-15", + "effective_name": "John Michael Doe", + "status": "ACTIVE", + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": ["USA"], + "approved_emails": ["john@example.com"], + "approved_phones": ["+14155551234"], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved", + }, + "features_list": [ + {"feature": "ID_VERIFICATION", "status": "Approved"}, + {"feature": "LIVENESS", "status": "Approved"}, + {"feature": "FACE_MATCH", "status": "Approved"}, + {"feature": "AML", "status": "Approved"}, + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "name": "VIP", + "color": "#2567FF", + } + ], + "created_at": "2025-06-01T08:00:00Z", + }, + { + "didit_internal_id": "6ed99d53-e8f5-4cf8-9ac1-4a506cb40f6b", + "vendor_data": None, + "display_name": "Jane S.", + "full_name": "Jane Elizabeth Smith", + "date_of_birth": "1985-11-22", + "effective_name": "Jane S.", + "status": "FLAGGED", + "portrait_image_url": None, + "session_count": 1, + "approved_count": 0, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": [], + "approved_emails": [], + "approved_phones": [], + "features": {"ID_VERIFICATION": "In Review"}, + "features_list": [{"feature": "ID_VERIFICATION", "status": "In Review"}], + "last_session_at": "2025-06-20T14:00:00Z", + "last_activity_at": "2025-06-20T14:05:00Z", + "first_session_at": "2025-06-20T14:00:00Z", + "tags": [], + "created_at": "2025-06-20T14:00:00Z", + }, + ], + }, + ), + lambda c: c.users.list(limit=1, offset=1), + lambda c: c.users.list(limit=1, offset=1), + lambda r: isinstance(r, SyncPage) and all(isinstance(x1, UserListItem) for x1 in r.results), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, UserListItem) for x1 in r.results) + ), + ) + + +def test_users_list_networks() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/users/{vendor_user_uuid}/networks/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/users/00000000-0000-4000-8000-000000000001/networks/", + api="verification", + auth="api_key", + status=200, + response_json={ + "results": [ + { + "network": { + "uuid": "00000000-0000-4000-8000-000000000007", + "network_id": "sample", + "name": "sample", + "status": "active", + "risk": {}, + "pattern_types": ["exact_same_device"], + "signal_types": ["ip_address"], + "member_counts": {}, + "member_status": {}, + "signal_coverage": {}, + "first_activity_at": "2026-01-01T00:00:00Z", + "last_activity_at": "2026-01-01T00:00:00Z", + }, + "subject": {}, + "signal_types": ["ip_address"], + "signal_count": 1, + "joined_at": "2026-01-01T00:00:00Z", + "last_seen_at": "2026-01-01T00:00:00Z", + } + ] + }, + strict_type=UsersListNetworksResponse, + ), + lambda c: c.users.list_networks("00000000-0000-4000-8000-000000000001"), + lambda c: c.users.list_networks("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, UsersListNetworksResponse), + lambda r: isinstance(r, UsersListNetworksResponse), + ) + + +def test_users_retrieve() -> None: + """``GET /v3/users/{vendor_data}/``""" + check_operation( + Expect( + method="GET", + path="/v3/users/user-abc-123/", + api="verification", + auth="api_key", + status=200, + response_json={ + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": None, + "full_name": "John Michael Doe", + "date_of_birth": "1990-05-15", + "effective_name": "John Michael Doe", + "status": "ACTIVE", + "metadata": {"tier": "premium", "source": "website"}, + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": ["USA"], + "approved_emails": ["john@example.com"], + "approved_phones": ["+14155551234"], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved", + }, + "features_list": [ + {"feature": "ID_VERIFICATION", "status": "Approved"}, + {"feature": "LIVENESS", "status": "Approved"}, + {"feature": "FACE_MATCH", "status": "Approved"}, + {"feature": "AML", "status": "Approved"}, + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "tag": { + "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", + "name": "VIP", + "color": "#2567FF", + "description": None, + "source": "custom", + "created_at": "2025-05-01T09:00:00Z", + "updated_at": "2025-05-01T09:00:00Z", + }, + "added_by_email": "analyst@acme.com", + "added_by_name": "Jane Analyst", + "created_at": "2025-06-02T11:00:00Z", + } + ], + "comments": [ + { + "uuid": "c1111111-2222-4333-8444-555555555555", + "comment_type": "STATUS_CHANGED", + "comment": None, + "actor_email": "analyst@acme.com", + "actor_name": "Jane Analyst", + "previous_status": "FLAGGED", + "new_status": "ACTIVE", + "previous_value": None, + "new_value": None, + "changed_fields": [], + "metadata": None, + "mentioned_emails": [], + "created_at": "2025-06-01T10:30:00Z", + } + ], + "created_at": "2025-06-01T08:00:00Z", + "updated_at": "2025-06-15T10:30:00Z", + }, + ), + lambda c: c.users.retrieve("user-abc-123"), + lambda c: c.users.retrieve("user-abc-123"), + lambda r: isinstance(r, UserDetailItem), + lambda r: isinstance(r, UserDetailItem), + ) + + +def test_users_retrieve_ongoing_monitoring_job() -> None: + """``GET /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/{job_id}/``""" + check_operation( + Expect( + method="GET", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/vendor-users/toggle-ongoing-monitoring/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "job_id": "00000000-0000-4000-8000-000000000002", + "status": "PENDING", + "is_enabled": True, + "total": 1, + "processed": 1, + "outcomes": { + "enabled": 1, + "screened_and_enabled": 1, + "already_enabled": 1, + "disabled": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "skipped_sandbox": 1, + "failed_provider": 1, + "failed_insufficient_balance": 1, + }, + "error": "sample", + "estimate": { + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + }, + }, + strict_type=UsersRetrieveOngoingMonitoringJobResponse, + ), + lambda c: c.users.retrieve_ongoing_monitoring_job("00000000-0000-4000-8000-000000000001"), + lambda c: c.users.retrieve_ongoing_monitoring_job("00000000-0000-4000-8000-000000000001"), + lambda r: isinstance(r, UsersRetrieveOngoingMonitoringJobResponse), + lambda r: isinstance(r, UsersRetrieveOngoingMonitoringJobResponse), + ) + + +def test_users_set_aml_monitoring() -> None: + """``POST /v3/users/aml-monitoring/``""" + check_operation( + Expect( + method="POST", + path="/v3/users/aml-monitoring/", + api="verification", + auth="api_key", + json_body={ + "is_enabled": True, + "estimate_only": True, + "vendor_data_list": ["user-123", "user-456"], + }, + status=200, + response_json={ + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + "unmatched": ["sample"], + }, + ), + lambda c: c.users.set_aml_monitoring( + is_enabled=True, estimate_only=True, vendor_data_list=["user-123", "user-456"] + ), + lambda c: c.users.set_aml_monitoring( + is_enabled=True, estimate_only=True, vendor_data_list=["user-123", "user-456"] + ), + lambda r: ( + (isinstance(r, UsersSetAmlMonitoringResponseVariant1)) + or (isinstance(r, UsersSetAmlMonitoringResponseVariant2)) + or (isinstance(r, UsersSetAmlMonitoringResponse202)) + ), + lambda r: ( + (isinstance(r, UsersSetAmlMonitoringResponseVariant1)) + or (isinstance(r, UsersSetAmlMonitoringResponseVariant2)) + or (isinstance(r, UsersSetAmlMonitoringResponse202)) + ), + ) + + +def test_users_set_ongoing_monitoring() -> None: + """``PATCH /v3/organization/{organization_id}/application/{application_id}/vendor-users/toggle-ongoing-monitoring/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/vendor-users/toggle-ongoing-monitoring/", + api="verification", + auth="api_key", + json_body={"is_enabled": True}, + status=200, + response_json={ + "users_total": 1, + "already_enabled": 1, + "with_existing_aml": 1, + "needing_screening": 1, + "skipped_no_identity_data": 1, + "skipped_blocked": 1, + "estimated_screening_cost_usd": 0.2, + "estimated_annual_monitoring_cost_usd": 0.07, + "balance_sufficient": True, + }, + ), + lambda c: c.users.set_ongoing_monitoring(is_enabled=True), + lambda c: c.users.set_ongoing_monitoring(is_enabled=True), + lambda r: ( + (isinstance(r, UsersSetOngoingMonitoringResponseVariant1)) + or (isinstance(r, UsersSetOngoingMonitoringResponseVariant2)) + or (isinstance(r, UsersSetOngoingMonitoringResponse202)) + ), + lambda r: ( + (isinstance(r, UsersSetOngoingMonitoringResponseVariant1)) + or (isinstance(r, UsersSetOngoingMonitoringResponseVariant2)) + or (isinstance(r, UsersSetOngoingMonitoringResponse202)) + ), + ) + + +def test_users_update() -> None: + """``PATCH /v3/users/{vendor_data}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/users/user-abc-123/", + api="verification", + auth="api_key", + json_body={"display_name": "Jane S."}, + status=200, + response_json={ + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": "Jane S.", + "full_name": "Jane Elizabeth Smith", + "date_of_birth": "1985-11-22", + "effective_name": "Jane S.", + "status": "FLAGGED", + "metadata": {"tier": "premium"}, + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": ["USA"], + "approved_emails": ["john@example.com"], + "approved_phones": ["+14155551234"], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved", + }, + "features_list": [ + {"feature": "ID_VERIFICATION", "status": "Approved"}, + {"feature": "LIVENESS", "status": "Approved"}, + {"feature": "FACE_MATCH", "status": "Approved"}, + {"feature": "AML", "status": "Approved"}, + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "tag": { + "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", + "name": "VIP", + "color": "#2567FF", + "description": None, + "source": "custom", + "created_at": "2025-05-01T09:00:00Z", + "updated_at": "2025-05-01T09:00:00Z", + }, + "added_by_email": "analyst@acme.com", + "added_by_name": "Jane Analyst", + "created_at": "2025-06-02T11:00:00Z", + } + ], + "comments": [], + "created_at": "2025-06-01T08:00:00Z", + "updated_at": "2025-06-15T10:30:00Z", + }, + ), + lambda c: c.users.update("user-abc-123", display_name="Jane S."), + lambda c: c.users.update("user-abc-123", display_name="Jane S."), + lambda r: isinstance(r, UserDetailItem), + lambda r: isinstance(r, UserDetailItem), + ) + + +def test_users_update_status() -> None: + """``PATCH /v3/users/{vendor_data}/update-status/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/users/00000000-0000-4000-8000-000000000001/update-status/", + api="verification", + auth="api_key", + json_body={"status": "ACTIVE"}, + status=200, + response_json={ + "didit_internal_id": "f4e5e1f2-94a9-4f86-8c16-2b7d9b4db418", + "vendor_data": "user-abc-123", + "display_name": None, + "full_name": "John Michael Doe", + "date_of_birth": "1990-05-15", + "effective_name": "John Michael Doe", + "status": "BLOCKED", + "metadata": {"tier": "premium", "source": "website"}, + "portrait_image_url": "https:///...", + "session_count": 3, + "approved_count": 2, + "declined_count": 0, + "in_review_count": 1, + "issuing_states": ["USA"], + "approved_emails": ["john@example.com"], + "approved_phones": ["+14155551234"], + "features": { + "ID_VERIFICATION": "Approved", + "LIVENESS": "Approved", + "FACE_MATCH": "Approved", + "AML": "Approved", + }, + "features_list": [ + {"feature": "ID_VERIFICATION", "status": "Approved"}, + {"feature": "LIVENESS", "status": "Approved"}, + {"feature": "FACE_MATCH", "status": "Approved"}, + {"feature": "AML", "status": "Approved"}, + ], + "last_session_at": "2025-06-15T10:30:00Z", + "last_activity_at": "2025-06-15T10:30:00Z", + "first_session_at": "2025-06-01T08:00:00Z", + "tags": [ + { + "uuid": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", + "tag": { + "uuid": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b", + "name": "VIP", + "color": "#2567FF", + "description": None, + "source": "custom", + "created_at": "2025-05-01T09:00:00Z", + "updated_at": "2025-05-01T09:00:00Z", + }, + "added_by_email": "analyst@acme.com", + "added_by_name": "Jane Analyst", + "created_at": "2025-06-02T11:00:00Z", + } + ], + "comments": [ + { + "uuid": "c1111111-2222-4333-8444-555555555555", + "comment_type": "STATUS_CHANGED", + "comment": None, + "actor_email": None, + "actor_name": None, + "previous_status": "ACTIVE", + "new_status": "BLOCKED", + "previous_value": None, + "new_value": None, + "changed_fields": [], + "metadata": None, + "mentioned_emails": [], + "created_at": "2025-06-15T10:30:00Z", + } + ], + "created_at": "2025-06-01T08:00:00Z", + "updated_at": "2025-06-15T10:30:00Z", + }, + ), + lambda c: c.users.update_status("00000000-0000-4000-8000-000000000001", status="ACTIVE"), + lambda c: c.users.update_status("00000000-0000-4000-8000-000000000001", status="ACTIVE"), + lambda r: isinstance(r, UserDetailItem), + lambda r: isinstance(r, UserDetailItem), + ) + + +def test_users_upload_face() -> None: + """``POST /v3/organization/{organization_id}/application/{application_id}/vendor-users/by-id/{didit_internal_id}/faces/upload/``""" + check_operation( + Expect( + method="POST", + path="/v3/organization/0b5e8d2c-0000-4000-8000-0000000000aa/application/0b5e8d2c-0000-4000-8000-0000000000bb/vendor-users/by-id/00000000-0000-4000-8000-000000000001/faces/upload/", + api="verification", + auth="api_key", + json_body={ + "image": "/9j/4AAQSkZJRgABAQEA...", + "comment": "Imported from previous KYC provider", + }, + status=201, + response_json={ + "uuid": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "image_url": "https://signed-url.example/face.jpg", + "comment": "Imported from previous KYC provider", + "uploaded_by": "api", + "created_at": "2026-05-18T12:00:00Z", + }, + ), + lambda c: c.users.upload_face( + "00000000-0000-4000-8000-000000000001", + image="/9j/4AAQSkZJRgABAQEA...", + comment="Imported from previous KYC provider", + ), + lambda c: c.users.upload_face( + "00000000-0000-4000-8000-000000000001", + image="/9j/4AAQSkZJRgABAQEA...", + comment="Imported from previous KYC provider", + ), + lambda r: isinstance(r, UsersUploadFaceResponse), + lambda r: isinstance(r, UsersUploadFaceResponse), + ) diff --git a/tests/generated/test_wallet_screening.py b/tests/generated/test_wallet_screening.py new file mode 100644 index 0000000..737165f --- /dev/null +++ b/tests/generated/test_wallet_screening.py @@ -0,0 +1,266 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``wallet_screening`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.components.wallet_screening_result import WalletScreeningResult +from tests.harness import BINARY_BODY, Expect, check_operation + + +def test_wallet_screening_screen() -> None: + """``POST /v3/wallet-screening/``""" + check_operation( + Expect( + method="POST", + path="/v3/wallet-screening/", + api="verification", + auth="api_key", + json_body={ + "wallet_address": "0x94f1b9b64e2932f6a2db338f616844400cd58e8a", + "blockchain": "BTC", + }, + status=200, + response_json={ + "provider": "merklescience", + "screening_type": "WALLET_SCREENING", + "risk_score": 95, + "severity": "CRITICAL", + "status": "SCREENED", + "summary": "Merkle Science risk: Critical (5/5). SANCTIONS HIT. Dominant risk: sanctioned. Driven by: the address is attributed to Sanctions / OFAC (Example Sanctions)", + "wallet_address": "0x94f1b9b64e2932f6a2db338f616844400cd58e8a", + "transaction_hash": None, + "blockchain": "ETH", + "block_timestamp": None, + "total_value_usd": None, + "fee_usd": None, + "sender_rows": [], + "receiver_rows": [], + "transaction_flows": [], + "transaction_flows_status": None, + "transaction_flows_error": None, + "source_of_funds": [ + { + "category": "sanctioned", + "percentage": 100.0, + "amount_usd": 9813695.0, + "entity_name": "Example Sanctions", + "entity_type": "Sanctions", + "entity_subtype": "OFAC", + "hops": None, + "description": "Sanctions / OFAC", + "is_direct": True, + "exposure_direction": "incoming", + "exposure_type": "direct", + "provider_entity_id": "118885", + "provider_entity_type_id": "30", + "provider_entity_subtype_id": "125", + "country": "", + "raw_amount": None, + "raw_currency": None, + } + ], + "destination_of_funds": [ + { + "category": "defi", + "percentage": 84.824453, + "amount_usd": 7783414.38, + "entity_name": "Example DeFi 7", + "entity_type": "DeFi", + "entity_subtype": "Bridges", + "hops": None, + "description": "DeFi / Bridges", + "is_direct": False, + "exposure_direction": "outgoing", + "exposure_type": "indirect", + "provider_entity_id": "118459", + "provider_entity_type_id": "33", + "provider_entity_subtype_id": "143", + "country": "", + "raw_amount": None, + "raw_currency": None, + }, + { + "category": "defi", + "percentage": 15.175547, + "amount_usd": 1392494.37, + "entity_name": "Example DeFi 2", + "entity_type": "DeFi", + "entity_subtype": "Yield Aggregator", + "hops": None, + "description": "DeFi / Yield Aggregator", + "is_direct": False, + "exposure_direction": "outgoing", + "exposure_type": "indirect", + "provider_entity_id": "90192", + "provider_entity_type_id": "33", + "provider_entity_subtype_id": "142", + "country": "", + "raw_amount": None, + "raw_currency": None, + }, + ], + "counterparty_connections": [ + { + "entity_name": "Example Sanctions", + "entity_type": "Sanctions", + "entity_subtype": "OFAC", + "risk_level": "critical", + "categories": ["sanctioned"], + "received_usd": None, + "sent_usd": None, + "received_hops": None, + "sent_hops": None, + "percentage": 0.0, + "is_direct": True, + "country": None, + "provider_entity_id": None, + }, + { + "entity_name": "Example Sanctions", + "entity_type": "Sanctions", + "entity_subtype": "OFAC", + "risk_level": "critical", + "categories": ["sanctioned"], + "received_usd": None, + "sent_usd": None, + "received_hops": None, + "sent_hops": None, + "percentage": 0.0, + "is_direct": True, + "country": None, + "provider_entity_id": None, + }, + ], + "network_nodes": [], + "network_edges": [], + "sanctions_hit": True, + "pep_counterparty": None, + "dominant_risk_category": "sanctioned", + "risk_factors": [ + { + "source": "attribution", + "category": "sanctioned", + "label": "Sanctions / OFAC", + "entity_name": "Example Sanctions", + "direction": "attribution", + "exposure_type": "attribution", + "amount_usd": None, + "percentage": 0.0, + "is_high_risk": True, + "description": "the address is attributed to Sanctions / OFAC (Example Sanctions)", + }, + { + "source": "exposure", + "category": "sanctioned", + "label": "Sanctions / OFAC", + "entity_name": "Example Sanctions", + "direction": "incoming", + "exposure_type": "direct", + "amount_usd": 9813695.0, + "percentage": 51.679304, + "is_high_risk": True, + "description": "51.6793% direct incoming exposure to Sanctions / OFAC (Example Sanctions)", + }, + { + "source": "exposure", + "category": "defi", + "label": "DeFi / Bridges", + "entity_name": "Example DeFi 7", + "direction": "outgoing", + "exposure_type": "indirect", + "amount_usd": 7783414.38, + "percentage": 40.987766, + "is_high_risk": False, + "description": "40.9878% indirect outgoing exposure to DeFi / Bridges (Example DeFi 7)", + }, + { + "source": "exposure", + "category": "defi", + "label": "DeFi / Yield Aggregator", + "entity_name": "Example DeFi 2", + "direction": "outgoing", + "exposure_type": "indirect", + "amount_usd": 1392494.37, + "percentage": 7.33293, + "is_high_risk": False, + "description": "7.33293% indirect outgoing exposure to DeFi / Yield Aggregator (Example DeFi 2)", + }, + ], + "raw_response": {"...": "provider-specific details"}, + "report_signature": "", + }, + ), + lambda c: c.wallet_screening.screen( + wallet_address="0x94f1b9b64e2932f6a2db338f616844400cd58e8a", blockchain="BTC" + ), + lambda c: c.wallet_screening.screen( + wallet_address="0x94f1b9b64e2932f6a2db338f616844400cd58e8a", blockchain="BTC" + ), + lambda r: isinstance(r, WalletScreeningResult), + lambda r: isinstance(r, WalletScreeningResult), + ) + + +def test_wallet_screening_screen_pdf() -> None: + """``POST /v3/wallet-screening/pdf/``""" + check_operation( + Expect( + method="POST", + path="/v3/wallet-screening/pdf/", + api="verification", + auth="api_key", + json_body={ + "provider": "sample", + "screening_type": "WALLET_SCREENING", + "risk_score": 1, + "severity": "UNKNOWN", + "status": "SCREENED", + "summary": "sample", + "wallet_address": "sample", + "blockchain": "sample", + "source_of_funds": [{}], + "destination_of_funds": [{}], + "counterparty_connections": [{}], + "sanctions_hit": True, + "dominant_risk_category": "sample", + "report_signature": "sample", + }, + status=200, + response_bytes=BINARY_BODY, + response_content_type="application/pdf", + ), + lambda c: c.wallet_screening.screen_pdf( + provider="sample", + screening_type="WALLET_SCREENING", + risk_score=1, + severity="UNKNOWN", + status="SCREENED", + summary="sample", + wallet_address="sample", + blockchain="sample", + source_of_funds=[{}], + destination_of_funds=[{}], + counterparty_connections=[{}], + sanctions_hit=True, + dominant_risk_category="sample", + report_signature="sample", + ), + lambda c: c.wallet_screening.screen_pdf( + provider="sample", + screening_type="WALLET_SCREENING", + risk_score=1, + severity="UNKNOWN", + status="SCREENED", + summary="sample", + wallet_address="sample", + blockchain="sample", + source_of_funds=[{}], + destination_of_funds=[{}], + counterparty_connections=[{}], + sanctions_hit=True, + dominant_risk_category="sample", + report_signature="sample", + ), + lambda r: r == BINARY_BODY, + lambda r: r == BINARY_BODY, + ) diff --git a/tests/generated/test_webhooks.py b/tests/generated/test_webhooks.py new file mode 100644 index 0000000..e2de341 --- /dev/null +++ b/tests/generated/test_webhooks.py @@ -0,0 +1,216 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``webhooks`` resources: one per operation.""" + +from __future__ import annotations + +from didit.types.webhooks.destinations.create_response import WebhooksDestinationsCreateResponse +from didit.types.webhooks.destinations.list_response import WebhooksDestinationsListResponseItem +from didit.types.webhooks.destinations.retrieve_response import WebhooksDestinationsRetrieveResponse +from didit.types.webhooks.destinations.update_response import WebhooksDestinationsUpdateResponse +from tests.harness import Expect, check_operation + + +def test_webhooks_destinations_create() -> None: + """``POST /v3/webhook/destinations/``""" + check_operation( + Expect( + method="POST", + path="/v3/webhook/destinations/", + api="verification", + auth="api_key", + json_body={ + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "webhook_version": "v3", + "subscribed_events": ["status.updated", "data.updated"], + }, + status=201, + response_json={ + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": True, + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "subscribed_events": ["status.updated", "data.updated"], + "created_at": "2026-05-17T10:00:00Z", + "updated_at": "2026-05-17T10:00:00Z", + "summary": { + "total_deliveries": 0, + "failed_deliveries": 0, + "error_rate_percentage": 0, + "min_response_time_ms": None, + "avg_response_time_ms": None, + "max_response_time_ms": None, + "last_delivery_at": None, + }, + }, + ), + lambda c: c.webhooks.destinations.create( + label="Production session webhooks", + url="https://yourapp.com/webhooks/didit", + webhook_version="v3", + subscribed_events=["status.updated", "data.updated"], + ), + lambda c: c.webhooks.destinations.create( + label="Production session webhooks", + url="https://yourapp.com/webhooks/didit", + webhook_version="v3", + subscribed_events=["status.updated", "data.updated"], + ), + lambda r: isinstance(r, WebhooksDestinationsCreateResponse), + lambda r: isinstance(r, WebhooksDestinationsCreateResponse), + ) + + +def test_webhooks_destinations_delete() -> None: + """``DELETE /v3/webhook/destinations/{destination_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.webhooks.destinations.delete("11111111-2222-3333-4444-555555555555"), + lambda c: c.webhooks.destinations.delete("11111111-2222-3333-4444-555555555555"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_webhooks_destinations_list() -> None: + """``GET /v3/webhook/destinations/``""" + check_operation( + Expect( + method="GET", + path="/v3/webhook/destinations/", + api="verification", + auth="api_key", + status=200, + response_json=[ + { + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": True, + "webhook_version": "v3", + "subscribed_events": ["status.updated", "data.updated"], + "subscribed_events_count": 2, + "deliveries_count": 12453, + "failed_deliveries_count": 14, + "average_response_time_ms": 187, + "error_rate_percentage": 0, + "last_delivery_at": "2026-05-17T09:21:18Z", + "created_at": "2026-01-04T08:00:00Z", + "updated_at": "2026-04-22T14:09:11Z", + }, + { + "uuid": "22222222-3333-4444-5555-666666666666", + "label": "Staging mirror", + "url": "https://staging.yourapp.com/webhooks/didit", + "enabled": False, + "webhook_version": "v3", + "subscribed_events": ["status.updated"], + "subscribed_events_count": 1, + "deliveries_count": 0, + "failed_deliveries_count": 0, + "average_response_time_ms": None, + "error_rate_percentage": 0, + "last_delivery_at": None, + "created_at": "2026-03-12T10:15:02Z", + "updated_at": "2026-03-12T10:15:02Z", + }, + ], + ), + lambda c: c.webhooks.destinations.list(), + lambda c: c.webhooks.destinations.list(), + lambda r: ( + isinstance(r, list) + and all(isinstance(x1, WebhooksDestinationsListResponseItem) for x1 in r) + ), + lambda r: ( + isinstance(r, list) + and all(isinstance(x1, WebhooksDestinationsListResponseItem) for x1 in r) + ), + ) + + +def test_webhooks_destinations_retrieve() -> None: + """``GET /v3/webhook/destinations/{destination_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": True, + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "subscribed_events": ["status.updated", "data.updated"], + "created_at": "2026-01-04T08:00:00Z", + "updated_at": "2026-04-22T14:09:11Z", + "summary": { + "total_deliveries": 12453, + "failed_deliveries": 14, + "error_rate_percentage": 0, + "min_response_time_ms": 41, + "avg_response_time_ms": 187, + "max_response_time_ms": 2103, + "last_delivery_at": "2026-05-17T09:21:18Z", + }, + }, + ), + lambda c: c.webhooks.destinations.retrieve("11111111-2222-3333-4444-555555555555"), + lambda c: c.webhooks.destinations.retrieve("11111111-2222-3333-4444-555555555555"), + lambda r: isinstance(r, WebhooksDestinationsRetrieveResponse), + lambda r: isinstance(r, WebhooksDestinationsRetrieveResponse), + ) + + +def test_webhooks_destinations_update() -> None: + """``PATCH /v3/webhook/destinations/{destination_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/webhook/destinations/11111111-2222-3333-4444-555555555555/", + api="verification", + auth="api_key", + json_body={"enabled": False}, + status=200, + response_json={ + "uuid": "11111111-2222-3333-4444-555555555555", + "label": "Production session webhooks", + "url": "https://yourapp.com/webhooks/didit", + "enabled": False, + "webhook_version": "v3", + "secret_shared_key": "sample-secret", + "subscribed_events": ["status.updated", "data.updated"], + "created_at": "2026-01-04T08:00:00Z", + "updated_at": "2026-05-17T11:02:55Z", + "summary": { + "total_deliveries": 12453, + "failed_deliveries": 14, + "error_rate_percentage": 0, + "min_response_time_ms": 41, + "avg_response_time_ms": 187, + "max_response_time_ms": 2103, + "last_delivery_at": "2026-05-17T09:21:18Z", + }, + }, + ), + lambda c: c.webhooks.destinations.update( + "11111111-2222-3333-4444-555555555555", enabled=False + ), + lambda c: c.webhooks.destinations.update( + "11111111-2222-3333-4444-555555555555", enabled=False + ), + lambda r: isinstance(r, WebhooksDestinationsUpdateResponse), + lambda r: isinstance(r, WebhooksDestinationsUpdateResponse), + ) diff --git a/tests/generated/test_workflows.py b/tests/generated/test_workflows.py new file mode 100644 index 0000000..f512eb0 --- /dev/null +++ b/tests/generated/test_workflows.py @@ -0,0 +1,281 @@ +# Code generated by codegen from openapi/*.json. DO NOT EDIT. +"""Generated tests of the ``workflows`` resources: one per operation.""" + +from __future__ import annotations + +from didit import AsyncPage, SyncPage +from didit.types.components.workflow_list_item import WorkflowListItem +from tests.harness import Expect, check_operation + + +def test_workflows_create() -> None: + """``POST /v3/workflows/``""" + check_operation( + Expect( + method="POST", + path="/v3/workflows/", + api="verification", + auth="api_key", + json_body={ + "features": [ + {"feature": "OCR", "config": {"duplicated_user_action": "review"}}, + {"feature": "LIVENESS", "config": {"face_liveness_method": "passive"}}, + { + "feature": "FACE_MATCH", + "config": { + "face_match_score_decline_threshold": 40, + "face_match_score_review_threshold": 60, + }, + }, + ], + "workflow_label": "Standard KYC", + }, + status=201, + response_json={ + "uuid": "c3d4e5f6-7890-12cd-ef34-333333333333", + "workflow_id": "c3d4e5f6-7890-12cd-ef34-333333333333", + "workflow_label": "Standard KYC", + "workflow_type": None, + "is_default": False, + "is_archived": False, + "total_price": 0.15, + "min_price": 0.15, + "max_price": 0.15, + "features": "OCR + LIVENESS + FACE_MATCH", + "is_simple_workflow": False, + "is_editable": False, + "workflow_url": "https://verify.didit.me/u/w9TkPOx8RWG3UAz5ZRQGVQ", + "workflow_graph": { + "start_node": "ocr", + "nodes": "... one node per feature plus a final status node ...", + }, + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 1, + "status": "published", + "published_at": "2026-05-17T10:30:00Z", + "created_at": "2026-05-17T10:30:00Z", + "updated_at": "2026-05-17T10:30:00Z", + }, + ), + lambda c: c.workflows.create( + features=[ + {"feature": "OCR", "config": {"duplicated_user_action": "review"}}, + {"feature": "LIVENESS", "config": {"face_liveness_method": "passive"}}, + { + "feature": "FACE_MATCH", + "config": { + "face_match_score_decline_threshold": 40, + "face_match_score_review_threshold": 60, + }, + }, + ], + workflow_label="Standard KYC", + ), + lambda c: c.workflows.create( + features=[ + {"feature": "OCR", "config": {"duplicated_user_action": "review"}}, + {"feature": "LIVENESS", "config": {"face_liveness_method": "passive"}}, + { + "feature": "FACE_MATCH", + "config": { + "face_match_score_decline_threshold": 40, + "face_match_score_review_threshold": 60, + }, + }, + ], + workflow_label="Standard KYC", + ), + lambda r: isinstance(r, dict), + lambda r: isinstance(r, dict), + ) + + +def test_workflows_delete() -> None: + """``DELETE /v3/workflows/{settings_uuid}/``""" + check_operation( + Expect( + method="DELETE", + path="/v3/workflows/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=204, + ), + lambda c: c.workflows.delete("00000000-0000-4000-8000-000000000001"), + lambda c: c.workflows.delete("00000000-0000-4000-8000-000000000001"), + lambda r: r is None, + lambda r: r is None, + ) + + +def test_workflows_list() -> None: + """``GET /v3/workflows/``""" + check_operation( + Expect( + method="GET", + path="/v3/workflows/", + api="verification", + auth="api_key", + query={"limit": ["1"], "offset": ["1"]}, + status=200, + response_json={ + "count": 2, + "next": None, + "previous": None, + "results": [ + { + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_id": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_label": "Standard KYC", + "workflow_type": "kyc", + "is_default": True, + "is_archived": False, + "is_white_label_enabled": False, + "total_price": 0.15, + "min_price": 0.15, + "max_price": 0.15, + "features": "OCR + LIVENESS + FACE_MATCH", + "is_simple_workflow": True, + "is_editable": False, + "workflow_url": "https://verify.didit.me/u/_yljPOx8RWG3UAz5ZRQGVQ", + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 2, + "status": "published", + "has_draft": False, + "created_by": { + "name": "Alex Mateo", + "email": "alex@example.com", + "type": "user", + }, + "updated_at": "2026-04-12T10:30:00Z", + }, + { + "uuid": "b2c3d4e5-6789-01bc-defa-222222222222", + "workflow_id": "b2c3d4e5-6789-01bc-defa-222222222222", + "workflow_label": "Full Verification + AML", + "workflow_type": "kyc", + "is_default": False, + "is_archived": False, + "is_white_label_enabled": False, + "total_price": 0.25, + "min_price": 0.25, + "max_price": 0.25, + "features": "OCR + LIVENESS + FACE_MATCH + AML", + "is_simple_workflow": True, + "is_editable": True, + "workflow_url": "https://verify.didit.me/u/Aq3kPOx8RWG3UAz5ZRQGVQ", + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 1, + "status": "draft", + "has_draft": True, + "created_by": None, + "updated_at": "2026-04-15T18:05:21Z", + }, + ], + }, + ), + lambda c: c.workflows.list(limit=1, offset=1), + lambda c: c.workflows.list(limit=1, offset=1), + lambda r: ( + isinstance(r, SyncPage) and all(isinstance(x1, WorkflowListItem) for x1 in r.results) + ), + lambda r: ( + isinstance(r, AsyncPage) and all(isinstance(x1, WorkflowListItem) for x1 in r.results) + ), + ) + + +def test_workflows_retrieve() -> None: + """``GET /v3/workflows/{settings_uuid}/``""" + check_operation( + Expect( + method="GET", + path="/v3/workflows/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + status=200, + response_json={ + "uuid": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_id": "a1b2c3d4-5678-90ab-cdef-111111111111", + "workflow_label": "Standard KYC", + "workflow_type": "kyc", + "is_default": True, + "is_archived": False, + "is_liveness_enabled": True, + "face_liveness_method": "passive", + "face_liveness_score_decline_threshold": 50, + "face_liveness_score_review_threshold": 70, + "is_face_match_enabled": True, + "face_match_score_decline_threshold": 40, + "face_match_score_review_threshold": 60, + "is_aml_enabled": False, + "is_phone_verification_enabled": False, + "is_email_verification_enabled": False, + "is_database_validation_enabled": False, + "is_ip_analysis_enabled": False, + "is_nfc_enabled": False, + "duplicated_user_action": "no_action", + "documents_allowed": { + "ESP": { + "P": { + "sides": 1, + "enabled": 1, + "subtypes": ["EPASSPORT", "PASSPORT_GENERIC"], + "preferred_characters": "latin", + "expiration_check_mode": "strict", + } + } + }, + "max_retry_attempts": 3, + "retry_window_days": 7, + "session_expiration_time": 604800, + "version": 2, + "status": "published", + "published_at": "2026-04-12T10:30:00Z", + "is_simple_workflow": False, + "is_editable": False, + "workflow_url": "https://verify.didit.me/u/_yljPOx8RWG3UAz5ZRQGVQ", + "total_price": 0.15, + "min_price": 0.15, + "max_price": 0.15, + "features": "OCR + LIVENESS + FACE_MATCH", + "created_at": "2025-01-15T10:30:00Z", + "updated_at": "2026-04-12T10:30:00Z", + }, + ), + lambda c: c.workflows.retrieve("00000000-0000-4000-8000-000000000001"), + lambda c: c.workflows.retrieve("00000000-0000-4000-8000-000000000001"), + lambda r: True, + lambda r: True, + ) + + +def test_workflows_update() -> None: + """``PATCH /v3/workflows/{settings_uuid}/``""" + check_operation( + Expect( + method="PATCH", + path="/v3/workflows/00000000-0000-4000-8000-000000000001/", + api="verification", + auth="api_key", + json_body={"features": [{"feature": "OCR"}, {"feature": "AML"}]}, + status=200, + response_json={}, + strict_type=dict[str, object], + ), + lambda c: c.workflows.update( + "00000000-0000-4000-8000-000000000001", + features=[{"feature": "OCR"}, {"feature": "AML"}], + ), + lambda c: c.workflows.update( + "00000000-0000-4000-8000-000000000001", + features=[{"feature": "OCR"}, {"feature": "AML"}], + ), + lambda r: isinstance(r, dict), + lambda r: isinstance(r, dict), + ) diff --git a/tests/harness.py b/tests/harness.py new file mode 100644 index 0000000..67e2253 --- /dev/null +++ b/tests/harness.py @@ -0,0 +1,139 @@ +"""The mocked HTTP layer behind the generated per-operation tests (tests/generated). + +Each generated test describes the request it expects and the response the +fake API returns; :func:`check_operation` runs the call on ``Didit`` and on +``AsyncDidit`` and checks both. No test here touches the network. +""" + +from __future__ import annotations + +import asyncio +import json +from collections.abc import Awaitable, Callable, Mapping +from dataclasses import dataclass, field +from typing import Any + +import httpx +import pydantic + +from didit import AsyncDidit, Didit + +TEST_API_KEY = "test-api-key" +TEST_ACCOUNT_TOKEN = "test-account-token" +ORG_ID = "0b5e8d2c-0000-4000-8000-0000000000aa" +APP_ID = "0b5e8d2c-0000-4000-8000-0000000000bb" +BINARY_BODY = b"%PDF-1.4\n% synthetic test document\n" + +_MISSING: Any = object() +_HOSTS = {"verification": ("verification.didit.me", ""), "account": ("apx.didit.me", "/auth/v2")} + + +@dataclass(frozen=True) +class Expect: + method: str + path: str + api: str + auth: str + status: int + query: Mapping[str, list[str]] = field(default_factory=lambda: {}) + json_body: object = _MISSING + multipart_fields: list[str] | None = None + multipart_files: list[str] | None = None + response_json: object = None + response_bytes: bytes | None = None + response_content_type: str | None = None + strict_type: object = None + + +def _response(expect: Expect) -> httpx.Response: + if expect.response_bytes is not None: + return httpx.Response( + expect.status, + content=expect.response_bytes, + headers={"content-type": expect.response_content_type or "application/octet-stream"}, + ) + if expect.response_json is None: + return httpx.Response(expect.status) + return httpx.Response( + expect.status, json=expect.response_json, headers={"x-request-id": "req_test"} + ) + + +def _check_request(request: httpx.Request, expect: Expect) -> None: + host, prefix = _HOSTS[expect.api] + assert request.method == expect.method + assert request.url.scheme == "https" + assert request.url.host == host + assert request.url.path == prefix + expect.path, (request.url.path, prefix + expect.path) + query: dict[str, list[str]] = {} + for key, value in request.url.params.multi_items(): + query.setdefault(key, []).append(value) + assert query == dict(expect.query), (query, expect.query) + + headers = request.headers + assert headers["user-agent"].startswith("didit-python/") + assert headers["x-didit-client"].startswith("python/") + if expect.auth == "api_key": + assert headers.get("x-api-key") == TEST_API_KEY + assert "authorization" not in headers + elif expect.auth == "account_token": + assert headers.get("authorization") == f"Bearer {TEST_ACCOUNT_TOKEN}" + assert "x-api-key" not in headers + else: + assert "x-api-key" not in headers + assert "authorization" not in headers + + content = request.read() + if expect.multipart_fields is not None or expect.multipart_files is not None: + assert headers["content-type"].startswith("multipart/form-data; boundary=") + text = content.decode("utf-8", "replace") + for name in expect.multipart_fields or []: + assert f'name="{name}"\r\n' in text, name + for name in expect.multipart_files or []: + assert f'name="{name}"; filename=' in text, name + elif expect.json_body is not _MISSING: + assert headers["content-type"] == "application/json" + assert json.loads(content) == expect.json_body + else: + assert content == b"" + + +def check_operation( + expect: Expect, + sync_call: Callable[[Didit], object], + async_call: Callable[[AsyncDidit], Awaitable[object]], + sync_check: Callable[[Any], bool], + async_check: Callable[[Any], bool], +) -> None: + seen: list[httpx.Request] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request) + return _response(expect) + + options: dict[str, Any] = { + "api_key": TEST_API_KEY, + "organization_id": ORG_ID, + "application_id": APP_ID, + "account_token": TEST_ACCOUNT_TOKEN, + "max_retries": 0, + } + with httpx.Client(transport=httpx.MockTransport(handler)) as http: + result = sync_call(Didit(http_client=http, **options)) + assert len(seen) == 1 + _check_request(seen[0], expect) + assert sync_check(result), result + + async def run() -> object: + async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http: + return await async_call(AsyncDidit(http_client=http, **options)) + + result = asyncio.run(run()) + assert len(seen) == 2 + _check_request(seen[1], expect) + assert async_check(result), result + + if expect.strict_type is not None: + # Synthesised from the schema, so it must also pass strict validation. + strict: Any = expect.strict_type + pydantic.TypeAdapter(strict).validate_python(expect.response_json) diff --git a/tests/mock_api.py b/tests/mock_api.py new file mode 100644 index 0000000..56d0838 --- /dev/null +++ b/tests/mock_api.py @@ -0,0 +1,84 @@ +"""A scripted fake API for the core tests: queue responses, inspect requests.""" + +from __future__ import annotations + +import json +from collections.abc import Callable +from typing import Any + +import httpx + +from didit import AsyncDidit, Didit + +API_KEY = "test-api-key" +ORG_ID = "0b5e8d2c-0000-4000-8000-0000000000aa" +APP_ID = "0b5e8d2c-0000-4000-8000-0000000000bb" + +Responder = Callable[[httpx.Request], httpx.Response] + + +class MockAPI: + """Answers requests from a queue; an exception in the queue is raised as a transport error.""" + + def __init__(self) -> None: + self.requests: list[httpx.Request] = [] + self._queue: list[httpx.Response | Exception | Responder] = [] + self.default: httpx.Response | None = None + + def add(self, *items: httpx.Response | Exception | Responder) -> MockAPI: + self._queue.extend(items) + return self + + def json(self, status: int = 200, body: Any = None, **headers: str) -> MockAPI: + return self.add( + httpx.Response(status, json=body if body is not None else {}, headers=headers) + ) + + def __call__(self, request: httpx.Request) -> httpx.Response: + request.read() + self.requests.append(request) + if not self._queue: + if self.default is not None: + return self.default + raise AssertionError(f"Unexpected request {request.method} {request.url}") + item = self._queue.pop(0) + if isinstance(item, Exception): + raise item + if callable(item) and not isinstance(item, httpx.Response): + return item(request) + return item + + @property + def last(self) -> httpx.Request: + return self.requests[-1] + + def last_json(self) -> Any: + return json.loads(self.last.content) + + def client(self, **options: Any) -> Didit: + options.setdefault("api_key", API_KEY) + options.setdefault("organization_id", ORG_ID) + options.setdefault("application_id", APP_ID) + return Didit(http_client=httpx.Client(transport=httpx.MockTransport(self)), **options) + + def async_client(self, **options: Any) -> AsyncDidit: + options.setdefault("api_key", API_KEY) + options.setdefault("organization_id", ORG_ID) + options.setdefault("application_id", APP_ID) + return AsyncDidit( + http_client=httpx.AsyncClient(transport=httpx.MockTransport(self)), **options + ) + + +SESSION = { + "session_id": "11111111-2222-3333-4444-555555555555", + "session_number": 7, + "session_token": "abc123", + "url": "https://verify.didit.me/session/abc123", + "vendor_data": "user-1", + "metadata": None, + "status": "Not Started", + "workflow_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "workflow_version": 1, + "callback": None, +} diff --git a/tests/test_authentication.py b/tests/test_authentication.py new file mode 100644 index 0000000..59bab0c --- /dev/null +++ b/tests/test_authentication.py @@ -0,0 +1,1651 @@ +"""didit.authentication (docs/DESIGN.md section 10) against a stubbed issuer. + +Keys are generated locally and every request goes to an httpx.MockTransport +(tests/authentication_helpers.py); nothing here touches the network. +""" + +from __future__ import annotations + +import asyncio +import datetime as dt +import hashlib +import hmac +import json +import logging +import subprocess +import sys +import threading +import urllib.parse +from collections.abc import Callable, Iterator +from concurrent.futures import ThreadPoolExecutor +from decimal import Decimal +from typing import Any, TypeVar, cast + +import httpx +import pytest +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +import didit +from didit import ( + APIConnectionError, + APIResponseValidationError, + APIStatusError, + APITimeoutError, + AuthenticationError, + BadRequestError, + ConfigurationError, + ConflictError, + InternalServerError, + NotFoundError, + PermissionDeniedError, + RateLimitError, +) +from didit.authentication import ( + AccessTokenClaims, + ActorClaim, + AsyncAuthentication, + Authentication, + AuthenticationSession, + IdTokenClaims, + OrganizationClaim, + PkcePair, + StepUp, + TokenResponse, + TokenVerificationError, + VerifiedClaims, + _crypto, + create_nonce, + create_pkce, + create_state, +) +from didit.authentication._cache import cache_seconds +from didit.authentication._issuer import issuer_from_publishable_key +from didit.authentication._pkce import s256_challenge +from tests.authentication_helpers import ( + ACCESS_TOKEN, + APPLICATION_ID, + CODE, + CODE_VERIFIER, + IDENTITY_ID, + ISSUER, + NONCE, + NOW, + REDIRECT_URI, + REFRESH_TOKEN, + REMOVE, + SESSION_ID, + STEP_UP_ID, + FakeClock, + FakeIssuer, + SigningKey, + access_claims, + b64url, + id_claims, + segment, + unsigned_token, +) + +AuthT = TypeVar("AuthT", Authentication, AsyncAuthentication) + +DISCOVERY = "/.well-known/openid-configuration" +JWKS = "/.well-known/jwks.json" +TOKEN_RESPONSE = { + "access_token": "synthetic.access.jwt", + "id_token": "synthetic.id.jwt", + "refresh_token": "synthetic-refresh-token-0002", + "token_type": "Bearer", + "expires_in": 300, + "session_id": SESSION_ID, +} +SESSION_RESPONSE = { + "sub": IDENTITY_ID, + "sid": SESSION_ID, + "application_id": APPLICATION_ID, + "vendor_data": "user-42", + "amr": ["passkey"], + "mfa_verified": True, + "email": "person@example.com", + "email_verified": True, + "phone": None, + "verified_identity": None, + "verified": {"level": "liveness", "liveness": True}, + "expires_at": "2027-01-15T10:00:00Z", + "post_logout_redirect_uri": "https://app.example.com/signed-out", + "organizations": [ + { + "organization_id": "6f1c2d3e-0000-4000-8000-00000000e001", + "organization__name": "Acme Treasury", + "role": "admin", + "custom_role": {"key": "approver", "permissions": ["payouts:approve"]}, + } + ], +} +STEP_UP_REQUIRED = { + "id": STEP_UP_ID, + "rule": "payout", + "method": "selfie", + "status": "pending", + "level": None, + "amount": "1500.00", + "currency": "EUR", + "created_at": "2027-01-15T10:00:00Z", + "completed_at": None, + "expires_at": "2027-01-15T10:30:00Z", + "required": True, + "verification_url": f"{ISSUER}/verify/?verification=synthetic-proof", + "expires_in": 1800, +} + + +def configure(auth: AuthT, clock: FakeClock) -> AuthT: + auth._cache.clock = clock + auth._now = lambda: float(NOW) + return auth + + +def reason(call: Callable[[], object]) -> str: + with pytest.raises(TokenVerificationError) as info: + call() + return info.value.reason + + +def form_of(request: httpx.Request) -> dict[str, str]: + return dict(urllib.parse.parse_qsl(request.content.decode("ascii"), strict_parsing=True)) + + +@pytest.fixture +def key() -> SigningKey: + return SigningKey.generate("k_synthetic_key_1") + + +@pytest.fixture +def issuer(key: SigningKey) -> FakeIssuer: + return FakeIssuer([key]) + + +@pytest.fixture +def clock() -> FakeClock: + return FakeClock() + + +@pytest.fixture +def auth(issuer: FakeIssuer, clock: FakeClock) -> Iterator[Authentication]: + with issuer.client() as http: + yield configure(Authentication(ISSUER, http_client=http), clock) + + +@pytest.fixture(scope="module") +def rsa_key() -> rsa.RSAPrivateKey: + return rsa.generate_private_key(public_exponent=65537, key_size=2048) + + +# Access tokens ----------------------------------------------------------------- + + +def test_verifies_an_access_token( + auth: Authentication, issuer: FakeIssuer, key: SigningKey +) -> None: + token = key.sign(access_claims(custom_claim="kept")) + claims = auth.verify_access_token(token) + assert isinstance(claims, AccessTokenClaims) + assert (claims.iss, claims.sub, claims.sid, claims.aud) == ( + ISSUER, + IDENTITY_ID, + SESSION_ID, + APPLICATION_ID, + ) + assert claims.client_id == APPLICATION_ID + assert (claims.iat, claims.exp, claims.jti) == (NOW, NOW + 300, "synthetic-jti-0001") + assert claims.vendor_data == "user-42" + assert claims.amr == ["passkey"] + assert isinstance(claims.org, OrganizationClaim) + assert (claims.org.id, claims.org.role) == ("6f1c2d3e-0000-4000-8000-00000000e001", "admin") + assert isinstance(claims.verified, VerifiedClaims) + assert (claims.verified.level, claims.verified.over_18) == ("full_id", True) + assert claims.act is None + assert claims.model_extra == {"custom_claim": "kept"} + assert [r.url.path for r in issuer.requests] == [DISCOVERY, JWKS] + + +def test_agent_access_token_carries_the_actor(auth: Authentication, key: SigningKey) -> None: + act = {"sub": "agent-0001", "agent": "Synthetic Agent", "scopes": ["payouts:read"]} + claims = auth.verify_access_token(key.sign(access_claims(act=act, amr=[], verified={}))) + assert isinstance(claims.act, ActorClaim) + assert (claims.act.sub, claims.act.agent, claims.act.scopes) == ( + "agent-0001", + "Synthetic Agent", + ["payouts:read"], + ) + assert claims.verified is not None + assert claims.verified.level is None + + +@pytest.mark.parametrize("typ", ["at+jwt", "AT+JWT", "application/at+jwt"]) +def test_access_token_type_is_case_insensitive( + auth: Authentication, key: SigningKey, typ: str +) -> None: + assert auth.verify_access_token(key.sign(access_claims(), typ=typ)).sub == IDENTITY_ID + + +@pytest.mark.parametrize("typ", [None, "JWT", "jwt", "id+jwt", "at+jwe"]) +def test_rejects_a_token_that_is_not_an_access_token( + auth: Authentication, key: SigningKey, typ: str | None +) -> None: + assert reason(lambda: auth.verify_access_token(key.sign(access_claims(), typ=typ))) == ( + "invalid_type" + ) + + +def test_audience_is_checked_when_given(auth: Authentication, key: SigningKey) -> None: + token = key.sign(access_claims()) + assert auth.verify_access_token(token, APPLICATION_ID).aud == APPLICATION_ID + assert auth.verify_access_token(token, ["other", APPLICATION_ID]).sub == IDENTITY_ID + assert reason(lambda: auth.verify_access_token(token, "other-application")) == ( + "invalid_audience" + ) + listed = key.sign(access_claims(aud=["other", APPLICATION_ID])) + assert auth.verify_access_token(listed, APPLICATION_ID).aud == ["other", APPLICATION_ID] + for odd in (7, {"aud": APPLICATION_ID}, [7, None]): + with pytest.raises(TokenVerificationError) as info: + auth.verify_access_token(key.sign(access_claims(aud=odd)), APPLICATION_ID) + assert info.value.reason == "invalid_audience" + no_aud = key.sign(access_claims(aud=REMOVE)) + assert auth.verify_access_token(no_aud).sub == IDENTITY_ID + assert reason(lambda: auth.verify_access_token(no_aud, APPLICATION_ID)) == "missing_claim" + with pytest.raises(ValueError, match="audience"): + auth.verify_access_token(token, []) + + +def test_client_id_is_the_default_audience( + issuer: FakeIssuer, clock: FakeClock, key: SigningKey +) -> None: + with issuer.client() as http: + auth = configure(Authentication(ISSUER, client_id=APPLICATION_ID, http_client=http), clock) + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + other = key.sign(access_claims(aud="another-application")) + assert reason(lambda: auth.verify_access_token(other)) == "invalid_audience" + assert auth.verify_access_token(other, "another-application").sub == IDENTITY_ID + + +@pytest.mark.parametrize("alg", ["HS256", "HS512", "RS256", "PS256", "EdDSA", "ES384", "es256"]) +def test_rejects_every_algorithm_but_es256( + auth: Authentication, issuer: FakeIssuer, key: SigningKey, alg: str +) -> None: + header = {"alg": alg, "typ": "at+jwt", "kid": key.kid} + token = unsigned_token(header, access_claims(), b"\x01" * 64) + assert reason(lambda: auth.verify_access_token(token)) == "unsupported_algorithm" + assert issuer.requests == [] + + +@pytest.mark.parametrize("header", [{"alg": "none", "kid": "k_synthetic_key_1"}, {}]) +def test_rejects_alg_none(auth: Authentication, issuer: FakeIssuer, header: dict[str, str]) -> None: + token = unsigned_token({**header, "typ": "at+jwt"}, access_claims()) + assert token.endswith(".") + assert reason(lambda: auth.verify_access_token(token)) == "unsupported_algorithm" + assert issuer.requests == [] + + +def test_rejects_hs256_signed_with_the_public_key(auth: Authentication, key: SigningKey) -> None: + # The classic key-confusion attack: an HMAC keyed with the published public key. + header = {"alg": "HS256", "typ": "at+jwt", "kid": key.kid} + signing_input = f"{segment(header)}.{segment(access_claims())}" + secret = json.dumps(key.jwk()).encode() + mac = hmac.new(secret, signing_input.encode(), hashlib.sha256).digest() + token = f"{signing_input}.{b64url(mac)}" + assert reason(lambda: auth.verify_access_token(token)) == "unsupported_algorithm" + + +def test_rejects_a_real_rs256_token( + auth: Authentication, key: SigningKey, rsa_key: rsa.RSAPrivateKey +) -> None: + header = {"alg": "RS256", "typ": "at+jwt", "kid": key.kid} + signing_input = f"{segment(header)}.{segment(access_claims())}" + signature = rsa_key.sign(signing_input.encode(), padding.PKCS1v15(), hashes.SHA256()) + token = f"{signing_input}.{b64url(signature)}" + assert reason(lambda: auth.verify_access_token(token)) == "unsupported_algorithm" + + +def test_rejects_a_token_signed_by_another_key(auth: Authentication, key: SigningKey) -> None: + impostor = SigningKey.generate(key.kid) + token = impostor.sign(access_claims()) + with pytest.raises(TokenVerificationError) as info: + auth.verify_access_token(token) + assert info.value.reason == "invalid_signature" + assert token not in str(info.value) + + +def test_rejects_tampered_claims_and_bad_signature_bytes( + auth: Authentication, key: SigningKey +) -> None: + header_b64, _, signature_b64 = key.sign(access_claims()).split(".") + forged = f"{header_b64}.{segment(access_claims(sub='someone-else'))}.{signature_b64}" + assert reason(lambda: auth.verify_access_token(forged)) == "invalid_signature" + short = f"{header_b64}.{segment(access_claims())}.{b64url(b'x' * 63)}" + assert reason(lambda: auth.verify_access_token(short)) == "invalid_signature" + empty = f"{header_b64}.{segment(access_claims())}." + assert reason(lambda: auth.verify_access_token(empty)) == "invalid_signature" + + +def test_rejects_the_wrong_issuer(auth: Authentication, key: SigningKey) -> None: + other = key.sign(access_claims(iss="https://other-0001.auth.didit.me")) + assert reason(lambda: auth.verify_access_token(other)) == "invalid_issuer" + slash = key.sign(access_claims(iss=ISSUER + "/")) + assert reason(lambda: auth.verify_access_token(slash)) == "invalid_issuer" + missing = key.sign(access_claims(iss=REMOVE)) + assert reason(lambda: auth.verify_access_token(missing)) == "missing_claim" + + +@pytest.mark.parametrize( + ("overrides", "expected"), + [ + ({"exp": NOW - 61}, "expired"), + ({"exp": NOW - 60}, "expired"), + ({"nbf": NOW + 120}, "not_yet_valid"), + ({"iat": NOW + 120}, "invalid_issued_at"), + ({"exp": REMOVE}, "missing_claim"), + ({"iat": REMOVE}, "missing_claim"), + ({"sub": REMOVE}, "missing_claim"), + ({"sub": ""}, "missing_claim"), + ({"exp": "soon"}, "malformed"), + ({"exp": True}, "malformed"), + ({"iat": [NOW]}, "malformed"), + ({"nbf": None}, "malformed"), + ], +) +def test_time_and_required_claims( + auth: Authentication, key: SigningKey, overrides: dict[str, Any], expected: str +) -> None: + assert ( + reason(lambda: auth.verify_access_token(key.sign(access_claims(**overrides)))) == expected + ) + + +@pytest.mark.parametrize( + "overrides", + [ + {"exp": NOW - 59}, + {"exp": NOW + 1.5}, + {"nbf": NOW + 59}, + {"iat": NOW + 59}, + {"nbf": NOW - 10}, + ], +) +def test_leeway_allows_sixty_seconds_of_skew( + auth: Authentication, key: SigningKey, overrides: dict[str, Any] +) -> None: + assert auth.verify_access_token(key.sign(access_claims(**overrides))).sub == IDENTITY_ID + + +def test_leeway_is_configurable(issuer: FakeIssuer, clock: FakeClock, key: SigningKey) -> None: + with issuer.client() as http: + strict = configure(Authentication(ISSUER, http_client=http, leeway=0), clock) + token = key.sign(access_claims(exp=NOW)) + assert reason(lambda: strict.verify_access_token(token)) == "expired" + lenient = configure(Authentication(ISSUER, http_client=http, leeway=600), clock) + old = key.sign(access_claims(exp=NOW - 500)) + assert lenient.verify_access_token(old).sub == IDENTITY_ID + + +def test_rejects_nan_in_a_signed_token(auth: Authentication, key: SigningKey) -> None: + token = key.sign({**access_claims(), "exp": float("nan")}) + assert reason(lambda: auth.verify_access_token(token)) == "malformed" + + +def _malformed_tokens(kid: str) -> list[object]: + good_header = segment({"alg": "ES256", "typ": "at+jwt", "kid": kid}) + payload = segment(access_claims()) + signature = b64url(b"\x00" * 64) + duplicate = b64url(b'{"alg": "ES256", "alg": "none"}') + not_utf8 = b64url(b"\xff\xfe") + deep = b64url(b"[" * 5000 + b"]" * 5000) + no_kid = segment({"alg": "ES256", "typ": "at+jwt"}) + bad_kid = segment({"alg": "ES256", "typ": "at+jwt", "kid": 7}) + critical = segment({"alg": "ES256", "typ": "at+jwt", "kid": kid, "crit": ["b64"]}) + return [ + "", + "abc", + "a.b", + "a.b.c.d", + "a.b.c.d.e", + f"!!!.{payload}.{signature}", + f"{good_header}=.{payload}.{signature}", + f"{b64url(b'not json')}.{payload}.{signature}", + f"{b64url(b'[1, 2]')}.{payload}.{signature}", + f"{duplicate}.{payload}.{signature}", + f"{not_utf8}.{payload}.{signature}", + f"{good_header}.{b64url(b'not json')}.{signature}", + f"{good_header}.{b64url(b'[]')}.{signature}", + f"{good_header}.abcde.{signature}", + f"{good_header}.{payload}.abc!", + f"{no_kid}.{payload}.{signature}", + f"{bad_kid}.{payload}.{signature}", + f"{critical}.{payload}.{signature}", + f"{good_header}.{deep}.{signature}", + "x" * 20000, + 123, + None, + b"a.b.c", + ] + + +def test_rejects_malformed_tokens( + auth: Authentication, issuer: FakeIssuer, key: SigningKey +) -> None: + for token in _malformed_tokens(key.kid): + with pytest.raises(TokenVerificationError) as info: + auth.verify_access_token(cast(Any, token)) + assert info.value.reason == "malformed", token + if isinstance(token, str) and len(token) > 10: + assert token not in str(info.value) + assert issuer.requests == [] + + +# Discovery and JWKS ------------------------------------------------------------ + + +def test_discovery_and_keys_are_fetched_lazily_and_cached( + auth: Authentication, issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + assert issuer.requests == [] + token = key.sign(access_claims()) + auth.verify_access_token(token) + auth.verify_access_token(token) + clock.advance(299) + auth.verify_access_token(token) + assert (issuer.count(DISCOVERY), issuer.count(JWKS)) == (1, 1) + clock.advance(2) + auth.verify_access_token(token) + assert (issuer.count(DISCOVERY), issuer.count(JWKS)) == (2, 2) + jwks_request = issuer.last(JWKS) + assert jwks_request.headers["accept"] == "application/json" + assert jwks_request.headers["user-agent"].startswith(f"didit-python/{didit.__version__} ") + assert jwks_request.headers["x-didit-client"] == f"python/{didit.__version__}" + assert "authorization" not in jwks_request.headers + assert "x-api-key" not in jwks_request.headers + + +@pytest.mark.parametrize( + ("cache_control", "fresh_for"), + [("public, max-age=10", 60), (None, 300), ("public, max-age=3600", 3600), ("no-store", 60)], +) +def test_key_set_is_cached_for_max_age_with_a_floor( + auth: Authentication, + issuer: FakeIssuer, + key: SigningKey, + clock: FakeClock, + cache_control: str | None, + fresh_for: int, +) -> None: + issuer.metadata_cache_control = cache_control + token = key.sign(access_claims()) + auth.verify_access_token(token) + clock.advance(fresh_for - 1) + auth.verify_access_token(token) + assert issuer.count(JWKS) == 1 + clock.advance(1) + auth.verify_access_token(token) + assert issuer.count(JWKS) == 2 + + +@pytest.mark.parametrize( + ("headers", "seconds"), + [ + ({}, 300), + ({"cache-control": "public, max-age=300"}, 300), + ({"cache-control": "max-age=120"}, 120), + ({"cache-control": 'MAX-AGE="900", public'}, 900), + ({"cache-control": "max-age=1"}, 60), + ({"cache-control": "max-age=0"}, 60), + ({"cache-control": "max-age=999999999"}, 86400), + ({"cache-control": "max-age=soon"}, 300), + ({"cache-control": "public, max-age=300", "age": "100"}, 200), + ({"cache-control": "public, max-age=300", "age": "290"}, 60), + ({"cache-control": "public, max-age=300", "age": "old"}, 300), + ({"cache-control": "max-age=300, no-cache"}, 60), + ({"cache-control": "no-store"}, 60), + ], +) +def test_cache_seconds(headers: dict[str, str], seconds: float) -> None: + assert cache_seconds(httpx.Headers(headers)) == seconds + + +def test_unknown_kid_refetches_at_most_once_a_minute( + auth: Authentication, issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + auth.verify_access_token(key.sign(access_claims())) + assert issuer.count(JWKS) == 1 + rotated = SigningKey.generate("k_synthetic_key_2") + rotated_token = rotated.sign(access_claims()) + clock.advance(1) + # The new key is not published yet: the refetch is refused for a minute after the last fetch. + assert reason(lambda: auth.verify_access_token(rotated_token)) == "unknown_key" + issuer.keys.append(rotated) + clock.advance(30) + assert reason(lambda: auth.verify_access_token(rotated_token)) == "unknown_key" + assert issuer.count(JWKS) == 1 + clock.advance(29) + assert auth.verify_access_token(rotated_token).sub == IDENTITY_ID + assert issuer.count(JWKS) == 2 + stranger = SigningKey.generate("k_never_published").sign(access_claims()) + for _ in range(5): + clock.advance(10) + assert reason(lambda: auth.verify_access_token(stranger)) == "unknown_key" + assert issuer.count(JWKS) == 2 + clock.advance(10) + assert reason(lambda: auth.verify_access_token(stranger)) == "unknown_key" + assert issuer.count(JWKS) == 3 + # Known keys keep verifying from the cache meanwhile. + auth.verify_access_token(key.sign(access_claims())) + assert issuer.count(JWKS) == 3 + + +@pytest.mark.parametrize( + "jwk_change", + [ + {"kty": "RSA"}, + {"crv": "P-384"}, + {"alg": "ES384"}, + {"use": "enc"}, + {"x": b64url(b"\x01" * 31)}, + {"y": 42}, + {"x": b64url(b"\x00" * 32), "y": b64url(b"\x00" * 32)}, + ], +) +def test_unusable_published_key_is_an_unknown_key( + auth: Authentication, issuer: FakeIssuer, key: SigningKey, jwk_change: dict[str, Any] +) -> None: + class Odd(SigningKey): + def jwk(self) -> dict[str, str]: + return {**super().jwk(), **jwk_change} + + issuer.keys[:] = [Odd(key.kid, key.private_key)] + assert reason(lambda: auth.verify_access_token(key.sign(access_claims()))) == "unknown_key" + + +def test_jwks_ignores_entries_without_a_kid( + auth: Authentication, issuer: FakeIssuer, key: SigningKey +) -> None: + issuer.route( + "GET", + "/keys/custom.json", + lambda _: httpx.Response( + 200, json={"keys": [{"kty": "EC"}, "junk", {**key.jwk(), "kid": ""}, key.jwk()]} + ), + ) + issuer.discovery["jwks_uri"] = f"{ISSUER}/keys/custom.json" + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + assert issuer.count("/keys/custom.json") == 1 + assert issuer.count(JWKS) == 0 + + +def test_documented_paths_are_used_when_discovery_omits_them( + auth: Authentication, issuer: FakeIssuer, key: SigningKey +) -> None: + issuer.discovery["jwks_uri"] = "https://[::1" + issuer.discovery["token_endpoint"] = 42 + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + assert auth.refresh(REFRESH_TOKEN).token_type == "Bearer" + assert [r.url.path for r in issuer.requests] == [DISCOVERY, JWKS, "/token/"] + + +def test_discovery_must_name_the_configured_issuer( + auth: Authentication, issuer: FakeIssuer, key: SigningKey +) -> None: + issuer.discovery["issuer"] = "https://login.example.com" + with pytest.raises(ConfigurationError, match=r"names the issuer 'https://login\.example\.com'"): + auth.verify_access_token(key.sign(access_claims())) + # OpenID Connect Discovery 1.0 section 4.3: the issuer must be present too. + del issuer.discovery["issuer"] + assert auth.refresh is not None + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + with pytest.raises(APIResponseValidationError, match="names no issuer"): + auth.refresh(REFRESH_TOKEN) + assert issuer.count("/token/") == 0 + issuer.discovery["issuer"] = ISSUER + assert auth.refresh(REFRESH_TOKEN).token_type == "Bearer" + + +@pytest.mark.parametrize( + ("field", "value", "message"), + [ + ("token_endpoint", "http://acme.auth.didit.me/token/", "does not use https"), + ("token_endpoint", "https://evil.example/steal", "not on the issuer's origin"), + ("token_endpoint", f"{ISSUER}:8443/token/", "not on the issuer's origin"), + ( + "jwks_uri", + f"http://{ISSUER.removeprefix('https://')}/.well-known/jwks.json", + "does not use https", + ), + ], +) +def test_discovery_endpoints_must_stay_on_https_and_the_issuer( + auth: Authentication, issuer: FakeIssuer, field: str, value: str, message: str +) -> None: + issuer.discovery[field] = value + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + with pytest.raises(APIResponseValidationError, match=message): + auth.refresh(REFRESH_TOKEN) + # Nothing went to the refused URL: only the discovery document was read. + assert [r.url.path for r in issuer.requests] == [DISCOVERY] + + +def test_keys_may_live_on_another_https_host( + issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + issuer.discovery["jwks_uri"] = "https://keys.example.com/jwks.json" + seen: list[str] = [] + + def handle(request: httpx.Request) -> httpx.Response: + seen.append(str(request.url)) + if request.url.host == "keys.example.com": + return httpx.Response(200, json={"keys": [key.jwk()]}) + return issuer.handle(request) + + with httpx.Client(transport=httpx.MockTransport(handle)) as http: + auth = configure(Authentication(ISSUER, http_client=http), clock) + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + assert seen[-1] == "https://keys.example.com/jwks.json" + + +def test_failed_key_fetches_are_rate_limited_too( + auth: Authentication, issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + keys_path = "/keys/flaky.json" + issuer.discovery["jwks_uri"] = f"{ISSUER}{keys_path}" + up = [False] + + def flaky(_: httpx.Request) -> httpx.Response: + if up[0]: + return httpx.Response(200, json={"keys": [key.jwk()]}) + return httpx.Response(500, json={"detail": "down"}) + + issuer.route("GET", keys_path, flaky) + with pytest.raises(InternalServerError): + auth.verify_access_token(key.sign(access_claims())) + # Random kids (an attacker, or a burst during an outage) do not refetch for a + # minute: they fail fast as a connection problem, not as an invalid token. + for index in range(10): + clock.advance(1) + stranger = SigningKey.generate(f"k_random_{index}").sign(access_claims()) + with pytest.raises(APIConnectionError, match="less than a minute ago") as info: + auth.verify_access_token(stranger) + assert not isinstance(info.value, TokenVerificationError) + assert info.value.request.url == f"{ISSUER}{keys_path}" + assert issuer.count(keys_path) == 1 + # Once a minute has passed the SDK tries again, and recovers when the issuer does. + clock.advance(50) + up[0] = True + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + assert issuer.count(keys_path) == 2 + # With a fresh key set, an unknown kid right after a failed refetch waits a minute too. + clock.advance(60) + up[0] = False + with pytest.raises(InternalServerError): + auth.verify_access_token(SigningKey.generate("k_new").sign(access_claims())) + clock.advance(1) + newer = SigningKey.generate("k_newer").sign(access_claims()) + assert reason(lambda: auth.verify_access_token(newer)) == "unknown_key" + assert issuer.count(keys_path) == 3 + # Known keys keep verifying from the cache meanwhile. + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + + +def test_async_failed_key_fetches_fail_fast( + issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + issuer.discovery["jwks_uri"] = f"{ISSUER}/keys/down.json" + issuer.reply("GET", "/keys/down.json", 503, {"detail": "down"}) + + async def run() -> None: + auth = configure(AsyncAuthentication(ISSUER, http_client=issuer.async_client()), clock) + with pytest.raises(InternalServerError): + await auth.verify_access_token(key.sign(access_claims())) + clock.advance(30) + with pytest.raises(APIConnectionError, match="less than a minute ago"): + await auth.verify_access_token(key.sign(access_claims())) + + asyncio.run(run()) + assert issuer.count("/keys/down.json") == 1 + + +def test_a_failed_discovery_counts_as_a_failed_key_fetch( + auth: Authentication, issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + discovery = issuer.discovery + issuer.discovery = cast(Any, None) # the issuer answers "null" + with pytest.raises(APIResponseValidationError): + auth.verify_access_token(key.sign(access_claims())) + with pytest.raises(APIConnectionError, match="less than a minute ago"): + auth.verify_access_token(key.sign(access_claims())) + assert issuer.count(DISCOVERY) == 1 + issuer.discovery = discovery + clock.advance(60) + assert auth.verify_access_token(key.sign(access_claims())).sub == IDENTITY_ID + + +def test_configuration_errors_and_cancellation_are_not_cached_as_failures( + issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + issuer.discovery["issuer"] = "https://login.example.com" + with issuer.client() as http: + auth = configure(Authentication(ISSUER, http_client=http), clock) + for _ in range(2): # raised every time, never masked by the failure cache + with pytest.raises(ConfigurationError): + auth.verify_access_token(key.sign(access_claims())) + assert issuer.count(DISCOVERY) == 2 + issuer.discovery["issuer"] = ISSUER + issuer.jwks_delay = 0.2 + + async def run() -> None: + auth = configure(AsyncAuthentication(ISSUER, http_client=issuer.async_client()), clock) + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(auth.verify_access_token(key.sign(access_claims())), 0.05) + # The cancelled fetch does not lock anyone out: the next call fetches again. + issuer.jwks_delay = 0.0 + assert (await auth.verify_access_token(key.sign(access_claims()))).sub == IDENTITY_ID + + asyncio.run(run()) + + +@pytest.mark.parametrize("claim", ["exp", "nbf", "iat"]) +def test_huge_numeric_dates_are_malformed( + auth: Authentication, key: SigningKey, claim: str +) -> None: + token = key.sign(access_claims(**{claim: 10**400})) + assert reason(lambda: auth.verify_access_token(token)) == "malformed" + + +def test_async_authentication_works_across_event_loops( + issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + async def verify_twice(auth: AsyncAuthentication) -> None: + tokens = [key.sign(access_claims()) for _ in range(2)] + await asyncio.gather(*(auth.verify_access_token(t) for t in tokens)) + + auth = configure(AsyncAuthentication(ISSUER, http_client=issuer.async_client()), clock) + asyncio.run(verify_twice(auth)) + clock.advance(600) # the cache expired: the next loop has to fetch, under its own lock + auth._cache._discovery = None + asyncio.run(verify_twice(auth)) + + +@pytest.mark.parametrize( + ("path", "response"), + [ + (DISCOVERY, httpx.Response(200, text="", headers={"content-type": "text/html"})), + (DISCOVERY, httpx.Response(200, json=["not", "an", "object"])), + (DISCOVERY, httpx.Response(200)), + (JWKS, httpx.Response(200, json={"keys": "none"})), + (JWKS, httpx.Response(200, json=[])), + ], +) +def test_unreadable_metadata_is_a_response_validation_error( + clock: FakeClock, key: SigningKey, path: str, response: httpx.Response +) -> None: + issuer = FakeIssuer([key]) + + def handle(request: httpx.Request) -> httpx.Response: + return response if request.url.path == path else issuer.handle(request) + + with httpx.Client(transport=httpx.MockTransport(handle)) as http: + auth = configure(Authentication(ISSUER, http_client=http), clock) + with pytest.raises(APIResponseValidationError): + auth.verify_access_token(key.sign(access_claims())) + + +def test_metadata_http_errors(clock: FakeClock, key: SigningKey) -> None: + requests: list[httpx.Request] = [] + + def failing(request: httpx.Request) -> httpx.Response: + requests.append(request) + return httpx.Response(503, text="upstream unavailable") + + with httpx.Client(transport=httpx.MockTransport(failing)) as http: + auth = configure(Authentication(ISSUER, http_client=http), clock) + with pytest.raises(InternalServerError) as info: + auth.verify_access_token(key.sign(access_claims())) + assert info.value.status == 503 + assert [r.url.path for r in requests] == [DISCOVERY] + assert str(info.value) == "HTTP 503: upstream unavailable" + + +def test_concurrent_threads_fetch_the_keys_once( + auth: Authentication, issuer: FakeIssuer, key: SigningKey +) -> None: + issuer.jwks_delay = 0.05 + tokens = [key.sign(access_claims(jti=f"jti-{n}")) for n in range(8)] + start = threading.Barrier(8, timeout=10) + + def verify(token: str) -> str: + start.wait() + return auth.verify_access_token(token).sub + + with ThreadPoolExecutor(max_workers=8) as pool: + subjects = list(pool.map(verify, tokens)) + assert subjects == [IDENTITY_ID] * 8 + assert (issuer.count(DISCOVERY), issuer.count(JWKS)) == (1, 1) + + +def test_import_error_without_cryptography( + auth: Authentication, key: SigningKey, monkeypatch: pytest.MonkeyPatch +) -> None: + for name in ( + "cryptography.exceptions", + "cryptography.hazmat.primitives", + "cryptography.hazmat.primitives.asymmetric", + "cryptography.hazmat.primitives.asymmetric.ec", + "cryptography.hazmat.primitives.asymmetric.utils", + ): + monkeypatch.setitem(sys.modules, name, None) + with pytest.raises(ImportError, match=r"pip install 'didit-sdk\[auth\]'"): + auth.verify_access_token(key.sign(access_claims())) + with pytest.raises(ImportError, match=r"didit-sdk\[auth\]"): + _crypto.verify_es256(cast(Any, None), b"", b"") + + +def test_module_imports_without_cryptography() -> None: + code = ( + "import sys\n" + "sys.modules['cryptography'] = None\n" + "import didit.authentication as a\n" + "pair = a.create_pkce()\n" + "auth = a.Authentication('https://acme.auth.didit.me')\n" + "auth.close()\n" + "print(pair.method, 'cryptography' in sys.modules and sys.modules['cryptography'] is None)\n" + ) + result = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True, check=False, timeout=120 + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "S256 True" + + +# ID tokens --------------------------------------------------------------------- + + +def test_verifies_an_id_token(auth: Authentication, key: SigningKey) -> None: + token = key.sign(id_claims(), typ="JWT") + claims = auth.verify_id_token(token, nonce=NONCE, audience=APPLICATION_ID) + assert isinstance(claims, IdTokenClaims) + assert (claims.sub, claims.sid, claims.nonce) == (IDENTITY_ID, SESSION_ID, NONCE) + assert (claims.email, claims.email_verified, claims.name) == ( + "person@example.com", + True, + "Synthetic Person", + ) + assert claims.auth_time == NOW - 10 + assert claims.phone_number is None + assert claims.org is None + + +@pytest.mark.parametrize("typ", ["JWT", "jwt", "application/jwt", None]) +def test_id_token_type(auth: Authentication, key: SigningKey, typ: str | None) -> None: + token = key.sign(id_claims(), typ=typ) + assert auth.verify_id_token(token, nonce=NONCE, audience=APPLICATION_ID).sub == IDENTITY_ID + + +@pytest.mark.parametrize("typ", ["at+jwt", "application/at+jwt", "dpop+jwt", 1]) +def test_an_access_token_is_never_an_id_token( + auth: Authentication, key: SigningKey, typ: Any +) -> None: + token = key.sign(id_claims(), header={"typ": typ}) + assert reason(lambda: auth.verify_id_token(token, nonce=NONCE, audience=APPLICATION_ID)) == ( + "invalid_type" + ) + + +def test_id_token_nonce(auth: Authentication, key: SigningKey) -> None: + token = key.sign(id_claims(), typ="JWT") + assert reason( + lambda: auth.verify_id_token(token, nonce="another-sign-in", audience=APPLICATION_ID) + ) == ("invalid_nonce") + no_nonce = key.sign(id_claims(nonce=REMOVE), typ="JWT") + assert reason(lambda: auth.verify_id_token(no_nonce, nonce=NONCE, audience=APPLICATION_ID)) == ( + "invalid_nonce" + ) + # An ID token from refresh() carries no nonce; the caller opts out explicitly. + refreshed = auth.verify_id_token(no_nonce, nonce=None, audience=APPLICATION_ID) + assert refreshed.nonce is None + with pytest.raises(ValueError, match="nonce"): + auth.verify_id_token(token, nonce="", audience=APPLICATION_ID) + + +def test_id_token_audience_is_required( + auth: Authentication, issuer: FakeIssuer, clock: FakeClock, key: SigningKey +) -> None: + token = key.sign(id_claims(), typ="JWT") + with pytest.raises(ConfigurationError, match="audience"): + auth.verify_id_token(token, nonce=NONCE) + assert issuer.requests == [] + assert reason(lambda: auth.verify_id_token(token, nonce=NONCE, audience="other")) == ( + "invalid_audience" + ) + no_aud = key.sign(id_claims(aud=REMOVE), typ="JWT") + assert reason(lambda: auth.verify_id_token(no_aud, nonce=NONCE, audience=APPLICATION_ID)) == ( + "missing_claim" + ) + with issuer.client() as http: + configured = configure( + Authentication(ISSUER, client_id=APPLICATION_ID, http_client=http), clock + ) + assert configured.verify_id_token(token, nonce=NONCE).aud == APPLICATION_ID + + +def test_id_token_time_checks(auth: Authentication, key: SigningKey) -> None: + expired = key.sign(id_claims(exp=NOW - 120), typ="JWT") + assert reason(lambda: auth.verify_id_token(expired, nonce=NONCE, audience=APPLICATION_ID)) == ( + "expired" + ) + other = SigningKey.generate(key.kid).sign(id_claims(), typ="JWT") + assert reason(lambda: auth.verify_id_token(other, nonce=NONCE, audience=APPLICATION_ID)) == ( + "invalid_signature" + ) + + +# Publishable keys and configuration --------------------------------------------- + + +def _pk(prefix: str, issuer: str) -> str: + return prefix + b64url(issuer.encode()) + + +def test_publishable_keys_carry_the_issuer(issuer: FakeIssuer) -> None: + assert issuer_from_publishable_key(_pk("pk_live_", ISSUER)) == ISSUER + sandbox = "https://acme-0001.sandbox.auth.didit.me" + assert issuer_from_publishable_key(_pk("pk_test_", sandbox)) == sandbox + assert issuer_from_publishable_key(" " + _pk("pk_live_", ISSUER + "/") + "\n") == ISSUER + assert issuer_from_publishable_key(_pk("pk_test_", "http://acme.auth.localhost:8000")) == ( + "http://acme.auth.localhost:8000" + ) + with issuer.client() as http: + auth = Authentication.from_publishable_key( + _pk("pk_live_", ISSUER), client_id=APPLICATION_ID, http_client=http, leeway=5 + ) + assert (auth.issuer, auth.client_id, auth.leeway) == (ISSUER, APPLICATION_ID, 5.0) + + +@pytest.mark.parametrize( + "publishable_key", + [ + "", + "pk_live_", + "pk_", + "sk_live_" + b64url(ISSUER.encode()), + "PK_LIVE_" + b64url(ISSUER.encode()), + "pk_live_!!!notbase64", + "pk_live_" + b64url(ISSUER.encode()) + "=", + "pk_live_a", + "pk_live_" + b64url(b"\xff\xfe\xfd"), + "pk_live_" + b64url(b"http://acme.auth.didit.me"), + "pk_test_" + b64url(b"http://acme.auth.didit.me"), + "pk_live_" + b64url(b"http://localhost:8000"), + "pk_live_" + b64url(b"acme.auth.didit.me"), + "pk_live_" + b64url(b"https://acme.auth.didit.me?x=1"), + cast(Any, None), + cast(Any, 42), + ], +) +def test_invalid_publishable_keys(publishable_key: str) -> None: + with pytest.raises(ConfigurationError): + issuer_from_publishable_key(publishable_key) + with pytest.raises(ConfigurationError): + AsyncAuthentication.from_publishable_key(publishable_key) + + +@pytest.mark.parametrize( + "value", + [ + "", + " ", + "acme.auth.didit.me", + "http://acme.auth.didit.me", + "ftp://acme.auth.didit.me", + "https://", + "https://user:secret@acme.auth.didit.me", + "https://acme.auth.didit.me/?x=1", + "https://acme.auth.didit.me#top", + "https://acme.auth.didit.me:99999", + "https://acme.auth.didit.me/a b", + "https://[::1", + cast(Any, None), + ], +) +def test_invalid_issuers(value: str) -> None: + with pytest.raises(ConfigurationError): + Authentication(value) + + +def test_issuer_normalisation_and_options() -> None: + with Authentication(ISSUER + "/") as auth: + assert auth.issuer == ISSUER + assert repr(auth) == f"Authentication(issuer={ISSUER!r}, client_id=None)" + assert auth.leeway == 60.0 + with Authentication("http://localhost:8000", client_id=f" {APPLICATION_ID} ") as local: + assert (local.issuer, local.client_id) == ("http://localhost:8000", APPLICATION_ID) + for bad in ("", " "): + with pytest.raises(ConfigurationError, match="client_id"): + Authentication(ISSUER, client_id=bad) + for leeway in (-1, cast(Any, True), cast(Any, "60"), float("nan")): + with pytest.raises(ConfigurationError, match="leeway"): + Authentication(ISSUER, leeway=leeway) + async_http = httpx.AsyncClient() + with pytest.raises(ConfigurationError, match=r"httpx\.Client"): + Authentication(ISSUER, http_client=cast(Any, async_http)) + asyncio.run(async_http.aclose()) + with ( + httpx.Client() as sync_http, + pytest.raises(ConfigurationError, match=r"httpx\.AsyncClient"), + ): + AsyncAuthentication(ISSUER, http_client=cast(Any, sync_http)) + + +def test_does_not_close_a_client_it_did_not_create(issuer: FakeIssuer) -> None: + with issuer.client() as http: + Authentication(ISSUER, http_client=http).close() + assert not http.is_closed + owned = Authentication(ISSUER) + owned.close() + assert owned._client.is_closed + + +# PKCE, state and nonce ----------------------------------------------------------- + + +def test_pkce_rfc7636_appendix_b() -> None: + verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" + assert s256_challenge(verifier) == "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" + + +def test_create_pkce() -> None: + pairs = [create_pkce() for _ in range(20)] + for pair in pairs: + assert isinstance(pair, PkcePair) + assert pair.method == "S256" + assert 43 <= len(pair.verifier) <= 128 + assert set(pair.verifier) <= set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~" + ) + assert pair.challenge == s256_challenge(pair.verifier) + assert len(pair.challenge) == 43 + assert pair.verifier not in repr(pair) + assert len({pair.verifier for pair in pairs}) == 20 + + +def test_state_and_nonce() -> None: + values = [create_state() for _ in range(10)] + [create_nonce() for _ in range(10)] + assert len(set(values)) == 20 + for value in values: + assert len(value) >= 43 + assert set(value) <= set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_") + + +def test_helpers_are_static_methods_too() -> None: + assert Authentication.create_pkce().method == "S256" + assert len(AsyncAuthentication.create_state()) >= 43 + with Authentication(ISSUER) as auth: + assert len(auth.create_nonce()) >= 43 + + +# Token, session and step-up requests --------------------------------------------- + + +def test_exchange_code(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + tokens = auth.exchange_code(CODE, code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI) + assert isinstance(tokens, TokenResponse) + assert tokens.access_token == "synthetic.access.jwt" + assert tokens.id_token == "synthetic.id.jwt" + assert tokens.refresh_token == "synthetic-refresh-token-0002" + assert (tokens.token_type, tokens.expires_in, tokens.session_id) == ("Bearer", 300, SESSION_ID) + for secret in ("synthetic.access.jwt", "synthetic.id.jwt", "synthetic-refresh-token-0002"): + assert secret not in repr(tokens) + assert [(r.method, r.url.path) for r in issuer.requests] == [ + ("GET", DISCOVERY), + ("POST", "/token/"), + ] + request = issuer.last("/token/") + assert str(request.url) == f"{ISSUER}/token/" + assert request.headers["content-type"] == "application/x-www-form-urlencoded" + assert request.headers["accept"] == "application/json" + assert request.headers["x-didit-client"] == f"python/{didit.__version__}" + assert "authorization" not in request.headers + assert "x-api-key" not in request.headers + assert form_of(request) == { + "grant_type": "authorization_code", + "code": CODE, + "code_verifier": CODE_VERIFIER, + "redirect_uri": REDIRECT_URI, + } + + +def test_token_requests_send_the_client_id_when_configured( + issuer: FakeIssuer, clock: FakeClock +) -> None: + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + with issuer.client() as http: + auth = configure(Authentication(ISSUER, client_id=APPLICATION_ID, http_client=http), clock) + auth.exchange_code(CODE, code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI) + assert form_of(issuer.last("/token/"))["client_id"] == APPLICATION_ID + auth.refresh(REFRESH_TOKEN) + assert form_of(issuer.last("/token/")) == { + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN, + "client_id": APPLICATION_ID, + } + assert issuer.count(DISCOVERY) == 1 + + +def test_refresh_uses_the_discovered_token_endpoint( + auth: Authentication, issuer: FakeIssuer +) -> None: + issuer.discovery["token_endpoint"] = f"{ISSUER}/oauth/token/" + issuer.reply("POST", "/oauth/token/", 200, TOKEN_RESPONSE) + tokens = auth.refresh(REFRESH_TOKEN) + assert tokens.refresh_token == "synthetic-refresh-token-0002" + assert form_of(issuer.last("/oauth/token/")) == { + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN, + } + + +def test_token_request_arguments_are_required(auth: Authentication, issuer: FakeIssuer) -> None: + with pytest.raises(ValueError, match="code is empty"): + auth.exchange_code("", code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI) + with pytest.raises(ValueError, match="code_verifier"): + auth.exchange_code(CODE, code_verifier=" ", redirect_uri=REDIRECT_URI) + with pytest.raises(ValueError, match="redirect_uri"): + auth.exchange_code(CODE, code_verifier=CODE_VERIFIER, redirect_uri=cast(Any, None)) + with pytest.raises(ValueError, match="refresh_token"): + auth.refresh("") + with pytest.raises(ValueError, match="access_token"): + auth.get_session("") + assert issuer.requests == [] + + +def test_get_session(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("GET", "/session/", 200, SESSION_RESPONSE) + session = auth.get_session(ACCESS_TOKEN) + assert isinstance(session, AuthenticationSession) + assert (session.sub, session.sid, session.application_id) == ( + IDENTITY_ID, + SESSION_ID, + APPLICATION_ID, + ) + assert session.expires_at == dt.datetime(2027, 1, 15, 10, 0, tzinfo=dt.timezone.utc) + assert session.verified is not None + assert session.verified.level == "liveness" + assert session.organizations is not None + organization = session.organizations[0] + assert (organization.organization_name, organization.role) == ("Acme Treasury", "admin") + assert organization.custom_role is not None + assert organization.custom_role.permissions == ["payouts:approve"] + assert session.to_dict()["organizations"][0]["organization__name"] == "Acme Treasury" + request = issuer.last("/session/") + assert (request.method, str(request.url)) == ("GET", f"{ISSUER}/session/") + assert request.headers["authorization"] == f"Bearer {ACCESS_TOKEN}" + assert request.headers["accept"] == "application/json" + assert request.content == b"" + assert issuer.count(DISCOVERY) == 0 + + +def test_revoke_session(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("DELETE", "/session/", 204) + auth.revoke_session(ACCESS_TOKEN) + request = issuer.last("/session/") + assert request.method == "DELETE" + assert request.headers["authorization"] == f"Bearer {ACCESS_TOKEN}" + + +def test_create_step_up(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("POST", "/step-up/", 201, STEP_UP_REQUIRED) + step_up = auth.create_step_up( + ACCESS_TOKEN, + action="payout", + amount=Decimal("1500.00"), + currency="EUR", + return_to="https://app.example.com/payouts", + ) + assert isinstance(step_up, StepUp) + assert (step_up.required, step_up.id, step_up.status, step_up.approved) == ( + True, + STEP_UP_ID, + "pending", + False, + ) + assert step_up.verification_url == f"{ISSUER}/verify/?verification=synthetic-proof" + assert (step_up.expires_in, step_up.amount, step_up.rule, step_up.method) == ( + 1800, + "1500.00", + "payout", + "selfie", + ) + assert step_up.expires_at == dt.datetime(2027, 1, 15, 10, 30, tzinfo=dt.timezone.utc) + request = issuer.last("/step-up/") + assert request.headers["authorization"] == f"Bearer {ACCESS_TOKEN}" + assert request.headers["content-type"] == "application/json" + assert json.loads(request.content) == { + "action": "payout", + "amount": "1500.00", + "currency": "EUR", + "return_to": "https://app.example.com/payouts", + } + + +def test_create_step_up_not_required(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("POST", "/step-up/", 200, {"required": False}) + step_up = auth.create_step_up( + ACCESS_TOKEN, action="contact", return_to="https://app.example.com/profile" + ) + assert (step_up.required, step_up.id, step_up.approved) == (False, None, False) + assert json.loads(issuer.last("/step-up/").content) == { + "action": "contact", + "return_to": "https://app.example.com/profile", + } + auth.create_step_up(ACCESS_TOKEN, action="bank") + assert json.loads(issuer.last("/step-up/").content) == {"action": "bank"} + + +@pytest.mark.parametrize( + ("amount", "sent"), + [ + ("1500.00", "1500.00"), + (1500, "1500"), + (1500.5, "1500.5"), + (0.1, "0.1"), + (Decimal("1E+3"), "1000"), + (Decimal("12.30"), "12.30"), + ], +) +def test_step_up_amounts_are_decimal_strings( + auth: Authentication, issuer: FakeIssuer, amount: Any, sent: str +) -> None: + issuer.reply("POST", "/step-up/", 200, {"required": False}) + auth.create_step_up(ACCESS_TOKEN, action="payout", amount=amount, return_to=REDIRECT_URI) + assert json.loads(issuer.last("/step-up/").content)["amount"] == sent + + +def test_step_up_argument_errors(auth: Authentication, issuer: FakeIssuer) -> None: + for amount in (True, [1], object()): + with pytest.raises(TypeError, match="amount"): + auth.create_step_up(ACCESS_TOKEN, action="payout", amount=cast(Any, amount)) + for bad in ( + float("nan"), + float("inf"), + Decimal("NaN"), + "Infinity", + "12,50", + "", + "1_000", + "1e3", + "\u0661\u0660", + "NaN", + ): + with pytest.raises(ValueError, match="amount"): + auth.create_step_up(ACCESS_TOKEN, action="payout", amount=bad) + with pytest.raises(ValueError, match="action"): + auth.create_step_up(ACCESS_TOKEN, action="") + with pytest.raises(ValueError, match="step_up_id"): + auth.retrieve_step_up(ACCESS_TOKEN, "") + with pytest.raises(ValueError, match="step_up_id"): + auth.retrieve_step_up(ACCESS_TOKEN, "..") + assert issuer.requests == [] + + +@pytest.mark.parametrize( + ("status", "approved"), + [ + ("approved", True), + ("pending", False), + ("review", False), + ("declined", False), + ("expired", False), + ("superseded", False), + ("some_future_status", False), + ], +) +def test_retrieve_step_up( + auth: Authentication, issuer: FakeIssuer, status: str, approved: bool +) -> None: + body = { + key: value + for key, value in STEP_UP_REQUIRED.items() + if key not in ("required", "verification_url", "expires_in") + } + body.update(status=status, level="liveness_face_match" if approved else None) + issuer.reply("GET", f"/step-up/{STEP_UP_ID}/", 200, body) + step_up = auth.retrieve_step_up(ACCESS_TOKEN, STEP_UP_ID) + assert (step_up.status, step_up.approved, step_up.required) == (status, approved, None) + request = issuer.last(f"/step-up/{STEP_UP_ID}/") + assert request.method == "GET" + assert request.headers["authorization"] == f"Bearer {ACCESS_TOKEN}" + + +def test_step_up_id_is_percent_encoded(auth: Authentication, issuer: FakeIssuer) -> None: + with pytest.raises(NotFoundError): + auth.retrieve_step_up(ACCESS_TOKEN, "a/b") + assert issuer.requests[-1].url.raw_path == b"/step-up/a%2Fb/" + + +# Errors -------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("status", "body", "error", "message"), + [ + (400, {"error": "invalid_grant"}, BadRequestError, "HTTP 400: invalid_grant"), + ( + 400, + {"error": "invalid_grant", "error_description": "The refresh token was already used."}, + BadRequestError, + "HTTP 400: invalid_grant: The refresh token was already used.", + ), + (400, {"non_field_errors": ["invalid_request"]}, BadRequestError, "HTTP 400"), + (401, {"detail": "invalid_token"}, AuthenticationError, "HTTP 401: invalid_token"), + ( + 403, + {"detail": "origin_not_allowed"}, + PermissionDeniedError, + "HTTP 403: origin_not_allowed", + ), + (404, {"detail": "Not found."}, NotFoundError, "HTTP 404: Not found."), + (409, {"detail": "conflict"}, ConflictError, "HTTP 409: conflict"), + (429, {"detail": "Too many requests."}, RateLimitError, "HTTP 429: Too many requests."), + (500, None, InternalServerError, "HTTP 500"), + (503, {"detail": "unavailable"}, InternalServerError, "HTTP 503: unavailable"), + (418, {"detail": "teapot"}, APIStatusError, "HTTP 418: teapot"), + ], +) +def test_status_errors( + auth: Authentication, + issuer: FakeIssuer, + status: int, + body: object, + error: type[APIStatusError], + message: str, +) -> None: + issuer.reply("POST", "/token/", status, body) + with pytest.raises(error) as info: + auth.refresh(REFRESH_TOKEN) + assert type(info.value) is error + assert info.value.status == status + assert info.value.body == body + assert info.value.message.startswith(message) + assert REFRESH_TOKEN not in str(info.value) + if body is not None: + assert info.value.request_id == "req_synthetic" + # Token requests are never retried. + assert issuer.count("/token/") == 1 + + +def test_errors_keep_no_bearer_token_on_their_request( + auth: Authentication, issuer: FakeIssuer +) -> None: + issuer.reply("GET", "/session/", 401, {"detail": "invalid_token"}) + with pytest.raises(AuthenticationError) as info: + auth.get_session(ACCESS_TOKEN) + assert info.value.response.request.headers["authorization"] == "[redacted]" + issuer.route("GET", "/session/", lambda _r: httpx.Response(200, text="")) + with pytest.raises(APIResponseValidationError) as html: + auth.get_session(ACCESS_TOKEN) + assert html.value.response.request.headers["authorization"] == "[redacted]" + + +def test_errors_keep_no_token_request_body(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("POST", "/token/", 400, {"error": "invalid_grant"}) + with pytest.raises(BadRequestError) as info: + auth.refresh(REFRESH_TOKEN) + kept = info.value.response.request + assert kept.url == f"{ISSUER}/token/" + assert REFRESH_TOKEN.encode() not in kept.read() + + def stall(_: httpx.Request) -> httpx.Response: + raise httpx.ReadTimeout("stalled") + + issuer.route("POST", "/token/", stall) + with pytest.raises(APITimeoutError) as timeout: + auth.exchange_code(CODE, code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI) + body = timeout.value.request.read() + assert CODE.encode() not in body + assert CODE_VERIFIER.encode() not in body + + +def test_echoed_secrets_are_redacted(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("POST", "/token/", 400, {"detail": f"bad token {REFRESH_TOKEN}"}) + with pytest.raises(BadRequestError) as info: + auth.refresh(REFRESH_TOKEN) + assert str(info.value) == "HTTP 400: bad token [redacted] (request id req_synthetic)" + issuer.reply("GET", "/session/", 401, {"detail": f"{ACCESS_TOKEN} is not valid"}) + with pytest.raises(AuthenticationError) as session_info: + auth.get_session(ACCESS_TOKEN) + assert ACCESS_TOKEN not in str(session_info.value) + issuer.reply("POST", "/token/", 400, {"detail": f"{CODE} / {CODE_VERIFIER}"}) + with pytest.raises(BadRequestError) as code_info: + auth.exchange_code(CODE, code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI) + assert CODE not in str(code_info.value) + assert CODE_VERIFIER not in str(code_info.value) + + +def test_session_errors_are_not_retried(auth: Authentication, issuer: FakeIssuer) -> None: + issuer.reply("GET", "/session/", 503, {"detail": "unavailable"}) + with pytest.raises(InternalServerError): + auth.get_session(ACCESS_TOKEN) + assert issuer.count("/session/") == 1 + + +@pytest.mark.parametrize( + "response", + [ + httpx.Response(200, text="", headers={"content-type": "text/html"}), + httpx.Response(200, json=["not", "an", "object"]), + httpx.Response(204), + ], +) +def test_unexpected_success_bodies( + auth: Authentication, issuer: FakeIssuer, response: httpx.Response +) -> None: + issuer.route("GET", "/session/", lambda _: response) + with pytest.raises(APIResponseValidationError) as info: + auth.get_session(ACCESS_TOKEN) + assert "/session/" in str(info.value) + + +@pytest.mark.parametrize( + ("exc", "error"), + [ + (httpx.ConnectError("refused"), APIConnectionError), + (httpx.ReadError("reset"), APIConnectionError), + (httpx.ConnectTimeout("slow"), APITimeoutError), + (httpx.ReadTimeout("slow"), APITimeoutError), + ], +) +def test_transport_errors( + clock: FakeClock, key: SigningKey, exc: httpx.HTTPError, error: type[APIConnectionError] +) -> None: + issuer = FakeIssuer([key]) + attempts: list[str] = [] + + def handle(request: httpx.Request) -> httpx.Response: + if request.url.path == "/token/": + attempts.append(request.method) + raise exc + return issuer.handle(request) + + with httpx.Client(transport=httpx.MockTransport(handle)) as http: + auth = configure(Authentication(ISSUER, http_client=http), clock) + with pytest.raises(error) as info: + auth.exchange_code(CODE, code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI) + assert type(info.value) is error + assert CODE not in str(info.value) + assert attempts == ["POST"] + + +def test_logs_never_contain_tokens( + auth: Authentication, issuer: FakeIssuer, caplog: pytest.LogCaptureFixture +) -> None: + issuer.reply("GET", "/session/", 200, SESSION_RESPONSE) + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + with caplog.at_level(logging.DEBUG, logger="didit"): + auth.get_session(ACCESS_TOKEN) + auth.refresh(REFRESH_TOKEN) + assert "GET /session/ -> 200" in caplog.text + assert "POST /token/ -> 200" in caplog.text + for secret in (ACCESS_TOKEN, REFRESH_TOKEN, "synthetic.access.jwt"): + assert secret not in caplog.text + + +# Async --------------------------------------------------------------------------- + + +def test_async_verifies_tokens(issuer: FakeIssuer, key: SigningKey, clock: FakeClock) -> None: + async def main() -> None: + async with issuer.async_client() as http: + auth = configure(AsyncAuthentication(ISSUER, http_client=http), clock) + claims = await auth.verify_access_token(key.sign(access_claims()), APPLICATION_ID) + assert isinstance(claims, AccessTokenClaims) + assert claims.sub == IDENTITY_ID + id_token = key.sign(id_claims(), typ="JWT") + id_claims_ = await auth.verify_id_token(id_token, nonce=NONCE, audience=APPLICATION_ID) + assert id_claims_.email == "person@example.com" + with pytest.raises(TokenVerificationError) as info: + await auth.verify_id_token(id_token, nonce="other", audience=APPLICATION_ID) + assert info.value.reason == "invalid_nonce" + with pytest.raises(TokenVerificationError) as alg: + await auth.verify_access_token( + unsigned_token({"alg": "none", "typ": "at+jwt"}, access_claims()) + ) + assert alg.value.reason == "unsupported_algorithm" + assert (issuer.count(DISCOVERY), issuer.count(JWKS)) == (1, 1) + + asyncio.run(main()) + + +def test_async_unknown_kid_and_caching( + issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + async def main() -> None: + async with issuer.async_client() as http: + auth = configure(AsyncAuthentication(ISSUER, http_client=http), clock) + await auth.verify_access_token(key.sign(access_claims())) + rotated = SigningKey.generate("k_synthetic_key_2") + issuer.keys.append(rotated) + clock.advance(10) + with pytest.raises(TokenVerificationError) as info: + await auth.verify_access_token(rotated.sign(access_claims())) + assert info.value.reason == "unknown_key" + clock.advance(50) + assert ( + await auth.verify_access_token(rotated.sign(access_claims())) + ).sub == IDENTITY_ID + assert issuer.count(JWKS) == 2 + clock.advance(300) + await auth.verify_access_token(key.sign(access_claims())) + assert (issuer.count(DISCOVERY), issuer.count(JWKS)) == (2, 3) + + asyncio.run(main()) + + +def test_async_concurrent_verifications_fetch_once( + issuer: FakeIssuer, key: SigningKey, clock: FakeClock +) -> None: + issuer.jwks_delay = 0.02 + + async def main() -> None: + async with issuer.async_client() as http: + auth = configure(AsyncAuthentication(ISSUER, http_client=http), clock) + tokens = [key.sign(access_claims(jti=f"jti-{n}")) for n in range(10)] + results = await asyncio.gather(*(auth.verify_access_token(t) for t in tokens)) + assert [claims.sub for claims in results] == [IDENTITY_ID] * 10 + assert (issuer.count(DISCOVERY), issuer.count(JWKS)) == (1, 1) + + asyncio.run(main()) + + +def test_async_requests(issuer: FakeIssuer, clock: FakeClock) -> None: + issuer.reply("POST", "/token/", 200, TOKEN_RESPONSE) + issuer.reply("GET", "/session/", 200, SESSION_RESPONSE) + issuer.reply("DELETE", "/session/", 204) + issuer.reply("POST", "/step-up/", 201, STEP_UP_REQUIRED) + issuer.reply("GET", f"/step-up/{STEP_UP_ID}/", 200, {**STEP_UP_REQUIRED, "status": "approved"}) + + async def main() -> None: + async with issuer.async_client() as http: + auth = configure( + AsyncAuthentication(ISSUER, client_id=APPLICATION_ID, http_client=http), clock + ) + tokens = await auth.exchange_code( + CODE, code_verifier=CODE_VERIFIER, redirect_uri=REDIRECT_URI + ) + assert tokens.session_id == SESSION_ID + assert form_of(issuer.last("/token/")) == { + "grant_type": "authorization_code", + "code": CODE, + "code_verifier": CODE_VERIFIER, + "redirect_uri": REDIRECT_URI, + "client_id": APPLICATION_ID, + } + refreshed = await auth.refresh(REFRESH_TOKEN) + assert refreshed.access_token == "synthetic.access.jwt" + assert form_of(issuer.last("/token/"))["grant_type"] == "refresh_token" + session = await auth.get_session(ACCESS_TOKEN) + assert session.sid == SESSION_ID + await auth.revoke_session(ACCESS_TOKEN) + assert issuer.requests[-1].method == "DELETE" + step_up = await auth.create_step_up( + ACCESS_TOKEN, action="payout", amount="1500.00", return_to=REDIRECT_URI + ) + assert step_up.required is True + assert json.loads(issuer.last("/step-up/").content) == { + "action": "payout", + "amount": "1500.00", + "return_to": REDIRECT_URI, + } + retrieved = await auth.retrieve_step_up(ACCESS_TOKEN, STEP_UP_ID) + assert retrieved.approved + for request in issuer.requests: + assert request.headers["x-didit-client"] == f"python/{didit.__version__}" + assert "x-api-key" not in request.headers + assert issuer.count(DISCOVERY) == 1 + + asyncio.run(main()) + + +def test_async_errors(key: SigningKey, clock: FakeClock) -> None: + issuer = FakeIssuer([key]) + issuer.reply("POST", "/token/", 400, {"error": "invalid_grant"}) + + async def handle(request: httpx.Request) -> httpx.Response: + if request.url.path == "/session/": + raise httpx.ReadTimeout("slow", request=request) + if request.url.path == f"/step-up/{STEP_UP_ID}/": + raise httpx.ConnectError("refused", request=request) + return issuer.handle(request) + + async def main() -> None: + async with httpx.AsyncClient(transport=httpx.MockTransport(handle)) as http: + auth = configure(AsyncAuthentication(ISSUER, http_client=http), clock) + with pytest.raises(BadRequestError, match="invalid_grant"): + await auth.refresh(REFRESH_TOKEN) + assert issuer.count("/token/") == 1 + with pytest.raises(APITimeoutError): + await auth.get_session(ACCESS_TOKEN) + with pytest.raises(APIConnectionError): + await auth.retrieve_step_up(ACCESS_TOKEN, STEP_UP_ID) + with pytest.raises(ConfigurationError, match="audience"): + await auth.verify_id_token("a.b.c", nonce=NONCE) + + asyncio.run(main()) + + +def test_async_lifecycle(issuer: FakeIssuer) -> None: + async def main() -> None: + async with AsyncAuthentication.from_publishable_key(_pk("pk_test_", ISSUER)) as owned: + assert owned.issuer == ISSUER + assert repr(owned) == f"AsyncAuthentication(issuer={ISSUER!r}, client_id=None)" + assert owned._client.is_closed + async with issuer.async_client() as http: + await AsyncAuthentication(ISSUER, http_client=http).close() + assert not http.is_closed + + asyncio.run(main()) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 0000000..5a61b3a --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,313 @@ +"""Client configuration, headers, application scope, redaction and logging (DESIGN 6).""" + +from __future__ import annotations + +import asyncio +import logging +import platform + +import httpx +import pytest + +import didit +from didit import AsyncDidit, ConfigurationError, Didit +from tests.mock_api import API_KEY, APP_ID, ORG_ID, SESSION, MockAPI + + +@pytest.fixture(autouse=True) +def _clean_env(monkeypatch: pytest.MonkeyPatch) -> None: + for name in ("DIDIT_API_KEY", "DIDIT_ORGANIZATION_ID", "DIDIT_APPLICATION_ID", "DIDIT_LOG"): + monkeypatch.delenv(name, raising=False) + + +def test_defaults() -> None: + client = Didit(api_key=API_KEY) + assert client.base_url == "https://verification.didit.me" + assert client.account_base_url == "https://apx.didit.me/auth/v2" + assert client.timeout == 60.0 + assert client.max_retries == 2 + assert client.organization_id is None + + +def test_environment_fallbacks(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("DIDIT_API_KEY", "env-key") + monkeypatch.setenv("DIDIT_ORGANIZATION_ID", "org-env") + monkeypatch.setenv("DIDIT_APPLICATION_ID", "app-env") + client = Didit() + assert (client.api_key, client.organization_id, client.application_id) == ( + "env-key", + "org-env", + "app-env", + ) + explicit = Didit(api_key="explicit", organization_id="o", application_id="a") + assert (explicit.api_key, explicit.organization_id, explicit.application_id) == ( + "explicit", + "o", + "a", + ) + asynchronous = AsyncDidit() + assert asynchronous.api_key == "env-key" + + +def test_missing_api_key_fails_before_sending() -> None: + api = MockAPI() + client = api.client(api_key=None) + with pytest.raises(ConfigurationError, match="DIDIT_API_KEY"): + client.sessions.retrieve_decision("s-1") + assert api.requests == [] + + +def test_invalid_options() -> None: + with pytest.raises(ConfigurationError): + Didit(api_key=API_KEY, base_url="verification.didit.me") + with pytest.raises(ConfigurationError): + Didit(api_key=API_KEY, max_retries=-1) + with pytest.raises(ConfigurationError): + Didit(http_client=httpx.AsyncClient()) # type: ignore[arg-type] + with pytest.raises(ConfigurationError): + AsyncDidit(http_client=httpx.Client()) # type: ignore[arg-type] + + +def test_headers_on_every_request() -> None: + api = MockAPI().json(201, SESSION) + client = api.client(default_headers={"X-Team": "risk"}) + client.sessions.create(workflow_id="w-1") + headers = api.last.headers + assert headers["x-api-key"] == API_KEY + assert headers["accept"] == "application/json" + assert headers["content-type"] == "application/json" + assert headers["x-didit-client"] == f"python/{didit.__version__}" + assert headers["user-agent"] == ( + f"didit-python/{didit.__version__} ({platform.python_implementation()} " + f"{platform.python_version()}; {platform.system()})" + ) + assert headers["x-team"] == "risk" + + +def test_extra_headers_add_and_remove() -> None: + api = MockAPI().json(201, SESSION) + client = api.client() + client.sessions.create( + workflow_id="w-1", + extra_headers={"X-Trace": "t-1", "X-Didit-Client": None}, + extra_query={"debug": True}, + extra_body={"new_field": 1}, + ) + assert api.last.headers["x-trace"] == "t-1" + assert "x-didit-client" not in api.last.headers + assert api.last.url.params["debug"] == "true" + assert api.last_json() == {"workflow_id": "w-1", "new_field": 1} + + +def test_header_names_are_merged_case_insensitively() -> None: + api = MockAPI().json(201, SESSION).json(201, SESSION) + client = api.client( + default_headers={"accept": "text/plain", "user-agent": "me/1", "X-API-KEY": "other"} + ) + client.sessions.create( + workflow_id="w-1", extra_headers={"content-type": "application/json; charset=utf-8"} + ) + raw = api.last.headers.raw + names = [name.lower() for name, _ in raw] + assert len(names) == len(set(names)), names + assert api.last.headers["accept"] == "text/plain" + assert api.last.headers["user-agent"] == "me/1" + assert api.last.headers["x-api-key"] == API_KEY # the client's key wins + assert api.last.headers["content-type"] == "application/json; charset=utf-8" + # Removing User-Agent removes it: httpx does not put its own one back. + client.sessions.create(workflow_id="w-1", extra_headers={"user-agent": None}) + assert "user-agent" not in api.last.headers + + +def test_bodies_json_cannot_carry_fail_before_sending() -> None: + api = MockAPI() + client = api.client() + for bad in (float("nan"), float("inf"), object()): + with pytest.raises(didit.DiditError, match="cannot be sent as JSON"): + client.sessions.create(workflow_id="w-1", metadata={"x": bad}) + assert api.requests == [] + + +def test_response_models_can_be_sent_back() -> None: + from didit._models import validate_response + from didit.types import SessionImportJob + + job = validate_response(SessionImportJob, {"uuid": "j-1", "status": "pending", "extra": 1}) + api = MockAPI().json(201, SESSION) + api.client().sessions.create(workflow_id="w-1", metadata={"job": job}) + assert api.last_json()["metadata"] == {"job": {"uuid": "j-1", "status": "pending", "extra": 1}} + + +def test_omitted_and_null_values() -> None: + api = MockAPI().json(201, SESSION) + client = api.client() + client.sessions.create(workflow_id="w-1", vendor_data=didit.NOT_GIVEN, metadata=None) + assert api.last_json() == {"workflow_id": "w-1", "metadata": None} + assert repr(didit.NOT_GIVEN) == "NOT_GIVEN" + assert not didit.NOT_GIVEN + assert didit.NotGiven() is didit.NOT_GIVEN + + +def test_application_scope_fills_ids_and_needs_them() -> None: + api = MockAPI().json(200, {"count": 0, "next": None, "previous": None, "results": []}) + client = api.client() + client.cases.list() + assert api.last.url.path == f"/v3/organization/{ORG_ID}/application/{APP_ID}/cases/" + for options in ({"organization_id": None}, {"application_id": None}): + unscoped = MockAPI() + with pytest.raises(ConfigurationError, match="organization_id and application_id"): + unscoped.client(**options).cases.list() + assert unscoped.requests == [] + + +def test_path_parameters_are_encoded_and_dot_segments_refused() -> None: + api = MockAPI().json(200, {}) + client = api.client() + client.sessions.retrieve_decision("a/b c?") + assert api.last.url.raw_path == b"/v3/session/a%2Fb%20c%3F/decision/" + for bad in ("", ".", ".."): + with pytest.raises(ValueError, match="non-empty id"): + client.sessions.retrieve_decision(bad) + + +def test_with_options_copies_and_shares_the_pool() -> None: + api = MockAPI().json(200, {}).json(200, {}) + client = api.client() + other = client.with_options(api_key="other-key", timeout=5, max_retries=0, organization_id="o2") + assert other.api_key == "other-key" + assert other.timeout == 5 + assert other.max_retries == 0 + assert other.organization_id == "o2" + assert other.application_id == client.application_id + assert other._client is client._client + assert client.api_key == API_KEY + other.sessions.retrieve_decision("s") + assert api.last.headers["x-api-key"] == "other-key" + assert client.copy(api_key=None).api_key is None + other.close() # does not close the shared pool + client.sessions.retrieve_decision("s") + + +def test_timeout_reaches_httpx() -> None: + seen: list[dict[str, float | None]] = [] + + def handler(request: httpx.Request) -> httpx.Response: + seen.append(request.extensions["timeout"]) + return httpx.Response(200, json={}) + + client = Didit( + api_key=API_KEY, + timeout=12.5, + http_client=httpx.Client(transport=httpx.MockTransport(handler)), + ) + client.sessions.retrieve_decision("s") + client.sessions.retrieve_decision("s", timeout=3) + client.sessions.retrieve_decision("s", timeout=httpx.Timeout(1.0, connect=2.0)) + assert seen[0]["read"] == 12.5 + assert seen[1]["read"] == 3 + assert seen[2]["connect"] == 2.0 + + +def test_account_api_uses_its_own_base_url_and_token() -> None: + api = MockAPI() + api.json(200, {"access_token": "acct-token", "refresh_token": "r", "expires_in": 86400}) + api.json(200, []) + client = api.client() + login = client.account.login(email="dev@example.com", password="pw") + request = api.last + assert request.url.host == "apx.didit.me" + assert request.url.path == "/auth/v2/programmatic/login/" + assert "x-api-key" not in request.headers + assert "authorization" not in request.headers + with pytest.raises(ConfigurationError, match="account token"): + client.account.organizations.list() + authed = client.with_options(account_token=login.access_token) + authed.account.organizations.list() + assert api.last.headers["authorization"] == "Bearer acct-token" + assert "x-api-key" not in api.last.headers + + +def test_account_methods_work_without_an_api_key() -> None: + api = MockAPI().json(200, {"access_token": "t"}) + Didit(api_key=None, http_client=httpx.Client(transport=httpx.MockTransport(api))).account.login( + email="a@example.com", password="p" + ) + assert len(api.requests) == 1 + + +def test_repr_never_shows_the_api_key() -> None: + client = Didit(api_key="super-secret-key") + assert "super-secret-key" not in repr(client) + assert "***" in repr(client) + assert "api_key=None" in repr(Didit(api_key=None)) + + +def _closed(client: Didit | AsyncDidit) -> bool: + return client._client.is_closed + + +def test_context_managers_close_owned_clients() -> None: + with Didit(api_key=API_KEY) as client: + assert not _closed(client) + assert _closed(client) + shared = httpx.Client() + with Didit(api_key=API_KEY, http_client=shared): + pass + assert not shared.is_closed + shared.close() + + async def run() -> AsyncDidit: + async with AsyncDidit(api_key=API_KEY) as aclient: + assert not _closed(aclient) + return aclient + + aclient = asyncio.run(run()) + assert _closed(aclient) + + +def test_async_with_options() -> None: + api = MockAPI().json(200, {}) + client = api.async_client() + other = client.with_options(api_key="k2") + + async def run() -> None: + await other.sessions.retrieve_decision("s") + await other.close() + + asyncio.run(run()) + assert api.last.headers["x-api-key"] == "k2" + assert other._client is client._client + + +def test_debug_logging_has_no_headers_or_bodies(caplog: pytest.LogCaptureFixture) -> None: + api = MockAPI().add(httpx.Response(201, json=SESSION, headers={"x-request-id": "req_42"})) + client = api.client() + with caplog.at_level(logging.DEBUG, logger="didit"): + client.sessions.create(workflow_id="w-1", vendor_data="private-user-ref") + text = caplog.text + assert "POST /v3/session/ -> 201" in text + assert "req_42" in text + assert API_KEY not in text + assert "private-user-ref" not in text + assert "w-1" not in text + + +def test_didit_log_environment_variable(monkeypatch: pytest.MonkeyPatch) -> None: + from didit._logging import logger, setup_logging + + monkeypatch.setenv("DIDIT_LOG", "debug") + before = list(logger.handlers) + try: + setup_logging() + setup_logging() # idempotent + added = [h for h in logger.handlers if h not in before] + assert len(added) == 1 + assert logger.level == logging.DEBUG + finally: + for handler in logger.handlers[:]: + if handler not in before: + logger.removeHandler(handler) + logger.setLevel(logging.NOTSET) + monkeypatch.setenv("DIDIT_LOG", "off") + setup_logging() + assert logger.handlers == before diff --git a/tests/test_contract.py b/tests/test_contract.py new file mode 100644 index 0000000..1689f5d --- /dev/null +++ b/tests/test_contract.py @@ -0,0 +1,188 @@ +"""Contract test (DESIGN 11): every ``server: true`` operation in openapi/ has exactly +one SDK method, on both clients, and the SDK has no other API methods. + +It reads the specs and the overlay directly, independently of the generator. +""" + +from __future__ import annotations + +import inspect +import json +import pathlib +import re +from typing import Any + +import pytest + +import didit +from didit import AsyncDidit, Didit +from didit._resource import AsyncAPIResource, SyncAPIResource + +OPENAPI = pathlib.Path(__file__).resolve().parents[1] / "openapi" +METHODS = ("get", "put", "post", "delete", "patch", "head", "options") +# Hand-written helpers that are not API operations. +CORE_METHODS = {("webhooks", "verify"), ("webhooks", "verify_parsed")} + + +def _deprecated() -> dict[str, dict[str, Any]]: + doc = json.loads((OPENAPI / "deprecated-operations.json").read_text("utf-8")) + return dict(doc["methods"]) + + +def _spec_operations() -> dict[tuple[str, str], dict[str, Any]]: + overlay = json.loads((OPENAPI / "overlay.json").read_text("utf-8"))["operations"] + out: dict[tuple[str, str], dict[str, Any]] = {} + raw_server_operations = 0 + for name, prefix in (("verification.json", ""), ("account.json", "account ")): + spec = json.loads((OPENAPI / name).read_text("utf-8")) + for path, item in spec["paths"].items(): + for method in METHODS: + op = item.get(method) + if op is None: + continue + key = f"{prefix}{method.upper()} {path}" + naming = op.get("x-didit-sdk") or overlay[key] + if not naming["server"]: + continue + raw_server_operations += 1 + name_key = (naming["resource"], naming["method"]) + assert name_key not in out, f"{key} duplicates {name_key}" + out[name_key] = {"key": key, "path": path, "naming": naming, "op": op, "item": item} + # Every server operation got its own name: none was lost to a duplicate. + assert len(out) == raw_server_operations + for name, entry in _deprecated().items(): + naming = entry["x-didit-sdk"] + name_key = (naming["resource"], naming["method"]) + assert name == ".".join(name_key) + assert name_key not in out, f"deprecated {name} is also a live method" + key = entry["operation_key"] + path = key.removeprefix("account ").split(" ", 1)[1] + out[name_key] = { + "key": key, + "path": path, + "naming": naming, + "op": entry["operation"], + "item": {}, + "deprecated": True, + } + return out + + +def _sdk_methods(root: Any, base: type) -> dict[tuple[str, str], Any]: + found: dict[tuple[str, str], Any] = {} + + def walk(resource: Any, path: str) -> None: + for name in dir(resource): + if name.startswith("_") or name in ("with_raw_response", "with_streaming_response"): + continue + value = getattr(resource, name) + if isinstance(value, base): + walk(value, f"{path}.{name}" if path else name) + elif callable(value): + found[(path, name)] = value + + for name in dir(type(root)): + if name.startswith("_"): + continue + value = getattr(root, name) + if isinstance(value, base): + walk(value, name) + return found + + +SPEC = _spec_operations() + + +def test_operation_counts() -> None: + # The numbers move with the API; the exact 1:1 mapping is checked below. + assert len(SPEC) >= 200 + assert len({resource for resource, _ in SPEC}) >= 40 + + +def _version_tuple(version: str) -> tuple[int, ...]: + return tuple(int(part) for part in re.findall(r"\d+", version)[:3]) + + +def test_deprecated_methods_expire_after_one_minor_version() -> None: + # DESIGN 3.1: a removed or renamed method stays deprecated for one minor + # version. The release that reaches remove_in deletes it. + current = _version_tuple(didit.__version__) + for name, entry in _deprecated().items(): + assert entry["reason"] in ("removed", "renamed"), name + assert current < _version_tuple(entry["remove_in"]), ( + f"{name} was due for removal in {entry['remove_in']}: delete its entry from " + "openapi/deprecated-operations.json and run codegen/generate.py" + ) + + +def test_deprecated_methods_warn() -> None: + client = Didit(api_key="contract") + sdk = _sdk_methods(client, SyncAPIResource) + for name, info in SPEC.items(): + marked = getattr(sdk[name], "__deprecated__", None) is not None + assert marked == bool(info.get("deprecated") or info["op"].get("deprecated")), name + + +@pytest.mark.parametrize( + ("client", "base"), + [ + (Didit(api_key="contract"), SyncAPIResource), + (AsyncDidit(api_key="contract"), AsyncAPIResource), + ], + ids=["Didit", "AsyncDidit"], +) +def test_spec_operations_equal_sdk_methods(client: Any, base: type) -> None: + sdk = _sdk_methods(client, base) + api_methods = {k for k in sdk if k not in CORE_METHODS} + missing = sorted(set(SPEC) - api_methods) + extra = sorted(api_methods - set(SPEC)) + assert not missing, f"operations without an SDK method: {missing}" + assert not extra, f"SDK methods without an operation: {extra}" + assert set(sdk) >= CORE_METHODS + + +def test_signatures_follow_the_paths() -> None: + client = Didit(api_key="contract") + sdk = _sdk_methods(client, SyncAPIResource) + for name, info in SPEC.items(): + params = inspect.signature(sdk[name]).parameters + positional = [ + p for p in params.values() if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + ] + path_names = re.findall(r"\{([^}]+)\}", info["path"]) + if info["naming"].get("scope") == "application": + assert info["path"].startswith( + "/v3/organization/{organization_id}/application/{application_id}/" + ) + path_names = [p for p in path_names if p not in ("organization_id", "application_id")] + assert len(positional) == len(path_names), name + for option in ("extra_headers", "extra_query", "extra_body", "timeout"): + assert option in params, (name, option) + has_idempotency = any( + p.get("in") == "header" and p.get("name", "").lower() == "idempotency-key" + for p in [*info["item"].get("parameters", []), *info["op"].get("parameters", [])] + ) + assert ("idempotency_key" in params) == has_idempotency, name + + +def test_async_methods_are_awaitable() -> None: + client = AsyncDidit(api_key="contract") + for name, method in _sdk_methods(client, AsyncAPIResource).items(): + if name in CORE_METHODS: + continue + returns = str(inspect.signature(method).return_annotation) + assert inspect.iscoroutinefunction(method) or returns.startswith("AsyncPaginator["), name + + +def test_device_and_public_operations_are_not_exposed() -> None: + overlay = json.loads((OPENAPI / "overlay.json").read_text("utf-8"))["operations"] + hidden = {(v["resource"], v["method"]) for v in overlay.values() if not v["server"]} + for name in ("verification.json", "account.json"): + for item in json.loads((OPENAPI / name).read_text("utf-8"))["paths"].values(): + for method in METHODS: + naming = (item.get(method) or {}).get("x-didit-sdk") + if naming and not naming["server"]: + hidden.add((naming["resource"], naming["method"])) + assert hidden + sdk = _sdk_methods(Didit(api_key="contract"), SyncAPIResource) + assert not hidden & set(sdk) diff --git a/tests/test_files.py b/tests/test_files.py new file mode 100644 index 0000000..26f6545 --- /dev/null +++ b/tests/test_files.py @@ -0,0 +1,268 @@ +"""Multipart uploads, binary downloads and raw or streamed responses (DESIGN 7 and 8).""" + +from __future__ import annotations + +import asyncio +import io +import mimetypes +import pathlib +import re + +import httpx +import pytest + +import didit +from didit._files import guess_content_type, to_httpx_file +from didit.types import SessionImportJob +from tests.mock_api import MockAPI + +PDF = b"%PDF-1.4\n% synthetic\n" +IMAGE = b"\x89PNG\r\n\x1a\nsynthetic" + + +def _parts(request: httpx.Request) -> dict[str, tuple[str | None, str | None, bytes]]: + """name -> (filename, content type, content) of a multipart body.""" + content_type = request.headers["content-type"] + boundary = content_type.split("boundary=")[1].encode() + out: dict[str, tuple[str | None, str | None, bytes]] = {} + for chunk in request.content.split(b"--" + boundary): + if b"\r\n\r\n" not in chunk: + continue + head, _, body = chunk.partition(b"\r\n\r\n") + text = head.decode() + name = re.search(r'name="([^"]+)"', text) + filename = re.search(r'filename="([^"]*)"', text) + ctype = re.search(r"Content-Type: (\S+)", text) + assert name is not None + out[name.group(1)] = ( + filename.group(1) if filename else None, + ctype.group(1) if ctype else None, + body.removesuffix(b"\r\n"), + ) + return out + + +def test_multipart_upload_with_bytes_path_and_stream(tmp_path: pathlib.Path) -> None: + back = tmp_path / "back.jpg" + back.write_bytes(IMAGE) + api = MockAPI().json(200, {"request_id": "r"}) + api.client().id_verification.verify( + front_image=("front.png", IMAGE, "image/png"), + back_image=back, + perform_document_liveness=False, + minimum_age=18, + metadata={"flow": "onboarding"}, + vendor_data="user-1", + ) + request = api.last + assert request.headers["content-type"].startswith("multipart/form-data") + assert "x-api-key" in request.headers + parts = _parts(request) + assert parts["front_image"] == ("front.png", "image/png", IMAGE) + assert parts["back_image"] == ("back.jpg", "image/jpeg", IMAGE) + assert parts["perform_document_liveness"][2] == b"false" + assert parts["minimum_age"][2] == b"18" + assert parts["metadata"][2] == b'{"flow":"onboarding"}' + assert parts["vendor_data"][2] == b"user-1" + assert parts["vendor_data"][0] is None + + +def test_streams_are_read_once_and_resent_on_retry() -> None: + stream = io.BytesIO(IMAGE) + stream.name = "/tmp/selfie.webp" + api = MockAPI().add(httpx.ConnectError("refused")).json(200, {}) + api.client().passive_liveness.check(user_image=stream) + assert len(api.requests) == 2 + for request in api.requests: + assert _parts(request)["user_image"] == ("selfie.webp", "image/webp", IMAGE) + + +def test_file_inputs() -> None: + assert to_httpx_file(b"x") == ("upload", b"x", "application/octet-stream") + assert to_httpx_file(bytearray(b"x")) == ("upload", b"x", "application/octet-stream") + assert to_httpx_file(("doc.pdf", PDF)) == ("doc.pdf", PDF, "application/pdf") + assert to_httpx_file((None, PDF, "application/pdf")) == ("upload", PDF, "application/pdf") + with pytest.raises(TypeError, match="binary mode"): + to_httpx_file(io.StringIO("text")) # type: ignore[arg-type] + with pytest.raises(TypeError, match="path, bytes or a binary stream"): + to_httpx_file(42) # type: ignore[arg-type] + with pytest.raises(TypeError, match="file tuple"): + to_httpx_file(("a", b"b", "c", "d")) # type: ignore[arg-type] + + +def test_content_types_do_not_depend_on_the_host(monkeypatch: pytest.MonkeyPatch) -> None: + # On Windows mimetypes reads the registry, which often lacks .webp and .heic. + monkeypatch.setattr(mimetypes, "guess_type", lambda *_a, **_k: (None, None)) + expected = { + "selfie.webp": "image/webp", + "front.JPG": "image/jpeg", + "back.jpeg": "image/jpeg", + "scan.tif": "image/tiff", + "scan.tiff": "image/tiff", + "card.png": "image/png", + "photo.heic": "image/heic", + "bill.pdf": "application/pdf", + "bundle.zip": "application/zip", + "users.csv": "text/csv", + "users.ndjson": "application/x-ndjson", + "notes.unknownext": "application/octet-stream", + } + for name, content_type in expected.items(): + assert guess_content_type(name) == content_type + assert to_httpx_file((name, b"x"))[2] == content_type + + +def test_json_or_multipart_operation_picks_the_encoding() -> None: + job = {"uuid": "j-1", "status": "pending", "import_type": "user_verification"} + api = MockAPI().json(201, job).json(201, job) + client = api.client() + result = client.sessions.imports.create(source_file_url="https://example.com/users.csv") + assert isinstance(result, SessionImportJob) + assert api.last.headers["content-type"] == "application/json" + assert api.last_json() == {"source_file_url": "https://example.com/users.csv"} + client.sessions.imports.create( + file=("users.csv", b"a,b\n1,2\n"), import_type="user_verification" + ) + parts = _parts(api.last) + assert parts["file"] == ("users.csv", "text/csv", b"a,b\n1,2\n") + assert parts["import_type"][2] == b"user_verification" + + +def test_multipart_without_files_is_still_multipart() -> None: + api = MockAPI().json(200, {}) + api.client().database_validation.validate( + issuing_state="BRA", services=["bra_cpf"], consent=True + ) + assert api.last.headers["content-type"] == "application/json" + assert api.last_json() == {"issuing_state": "BRA", "services": ["bra_cpf"], "consent": True} + api.json(200, {}) + api.client().database_validation.validate( + issuing_state="ARG", selfie=IMAGE, services=["arg_renaper"] + ) + parts = _parts(api.last) + assert parts["services"][2] == b'["arg_renaper"]' + assert parts["selfie"][0] == "upload" + + +def test_binary_response_returns_bytes() -> None: + api = MockAPI().add( + httpx.Response(200, content=PDF, headers={"content-type": "application/pdf"}) + ) + pdf = api.client().sessions.generate_pdf("s-1") + assert pdf == PDF + assert api.last.headers["accept"] == "application/pdf, application/json" + + +def test_raw_response() -> None: + api = MockAPI().add( + httpx.Response(200, json={"session_id": "s-1"}, headers={"x-request-id": "req_7"}) + ) + raw = api.client().sessions.with_raw_response.retrieve_decision("s-1") + assert isinstance(raw, didit.APIResponse) + assert raw.status_code == 200 + assert raw.request_id == "req_7" + assert raw.headers["x-request-id"] == "req_7" + assert raw.method == "GET" + assert raw.url.path == "/v3/session/s-1/decision/" + assert raw.retries_taken == 0 + assert raw.elapsed >= 0 + assert raw.json() == {"session_id": "s-1"} + assert "req_7" in repr(raw) + decision = raw.parse() + assert decision.session_id == "s-1" + assert raw.parse() is decision + assert raw.text() == '{"session_id":"s-1"}' + assert raw.read() == b'{"session_id":"s-1"}' + + +def test_raw_response_counts_retries() -> None: + api = MockAPI().add(httpx.Response(503)).json(200, {}) + raw = api.client().sessions.with_raw_response.retrieve_decision("s") + assert raw.retries_taken == 1 + + +def test_streaming_response(tmp_path: pathlib.Path) -> None: + api = MockAPI().add( + httpx.Response(200, content=PDF * 100, headers={"content-type": "application/pdf"}) + ) + target = tmp_path / "report.pdf" + with api.client().sessions.with_streaming_response.generate_pdf("s-1") as response: + assert response.status_code == 200 + response.write_to_file(str(target)) + assert target.read_bytes() == PDF * 100 + + api.add(httpx.Response(200, content=b"line1\nline2\n", headers={"content-type": "text/csv"})) + with api.client().sessions.with_streaming_response.generate_pdf("s-1") as response: + assert list(response.iter_lines()) == ["line1", "line2"] + api.add(httpx.Response(200, content=PDF)) + with api.client().sessions.with_streaming_response.generate_pdf("s-1") as response: + assert b"".join(response.iter_bytes()) == PDF + api.add(httpx.Response(200, content=b"abc")) + with api.client().sessions.with_streaming_response.generate_pdf("s-1") as response: + assert "".join(response.iter_text()) == "abc" + assert response.parse() == b"abc" + + +def test_streaming_error_is_raised_on_enter() -> None: + api = MockAPI().add(httpx.Response(404, json={"detail": "missing"})) + with ( + pytest.raises(didit.NotFoundError), + api.client().sessions.with_streaming_response.generate_pdf("s"), + ): + pass + + +def test_nested_raw_response_accessors() -> None: + job = {"uuid": "j-1"} + api = MockAPI().json(200, job) + raw = api.client().sessions.with_raw_response.imports.retrieve("j-1") + assert raw.parse().uuid == "j-1" + api.json(200, job) + with api.client().sessions.with_streaming_response.imports.retrieve("j-1") as streamed: + assert streamed.parse().uuid == "j-1" + + +def test_async_files_and_responses(tmp_path: pathlib.Path) -> None: + api = MockAPI() + api.json(200, {}) + api.add(httpx.Response(200, content=PDF, headers={"content-type": "application/pdf"})) + api.add(httpx.Response(200, json={"session_id": "s"}, headers={"x-request-id": "req_a"})) + api.add(httpx.Response(200, content=PDF)) + api.add(httpx.Response(200, content=b"a\nb\n")) + api.add(httpx.Response(200, content=b"xyz")) + target = tmp_path / "async.pdf" + + async def run() -> None: + client = api.async_client() + await client.face_match.compare(user_image=IMAGE, ref_image=("ref.jpg", IMAGE)) + assert "ref_image" in _parts(api.last) + assert await client.sessions.generate_pdf("s") == PDF + raw = await client.sessions.with_raw_response.retrieve_decision("s") + assert isinstance(raw, didit.AsyncAPIResponse) + assert raw.request_id == "req_a" + assert (await raw.parse()).session_id == "s" + assert await raw.json() == {"session_id": "s"} + assert await raw.text() == '{"session_id":"s"}' + assert await raw.read() == b'{"session_id":"s"}' + async with client.sessions.with_streaming_response.generate_pdf("s") as response: + await response.write_to_file(str(target)) + async with client.sessions.with_streaming_response.generate_pdf("s") as response: + assert [line async for line in response.iter_lines()] == ["a", "b"] + async with client.sessions.with_streaming_response.generate_pdf("s") as response: + assert "".join([t async for t in response.iter_text()]) == "xyz" + + asyncio.run(run()) + assert target.read_bytes() == PDF + + +def test_async_streamed_bytes() -> None: + api = MockAPI().add(httpx.Response(200, content=PDF)).json(200, {"uuid": "j"}) + + async def run() -> None: + client = api.async_client() + async with client.sessions.with_streaming_response.generate_pdf("s") as response: + assert b"".join([c async for c in response.iter_bytes()]) == PDF + async with client.sessions.with_streaming_response.imports.retrieve("j") as streamed: + assert (await streamed.parse()).uuid == "j" + + asyncio.run(run()) diff --git a/tests/test_http.py b/tests/test_http.py new file mode 100644 index 0000000..bc70061 --- /dev/null +++ b/tests/test_http.py @@ -0,0 +1,438 @@ +"""Errors, the retry policy, Retry-After and timeouts (DESIGN 7).""" + +from __future__ import annotations + +import asyncio +import email.utils +import time +from collections.abc import AsyncIterator, Iterator +from typing import Any + +import httpx +import pytest + +import didit +from didit._base_client import ( + parse_retry_after, + retry_delay, + should_retry_exception, + should_retry_status, +) +from tests.mock_api import API_KEY, SESSION, MockAPI + + +@pytest.fixture(autouse=True) +def no_sleep(monkeypatch: pytest.MonkeyPatch) -> list[float]: + delays: list[float] = [] + monkeypatch.setattr("didit._base_client.time.sleep", delays.append) + + async def fake_async_sleep(delay: float) -> None: + delays.append(delay) + + monkeypatch.setattr("didit._base_client.asyncio.sleep", fake_async_sleep) + return delays + + +def _request() -> httpx.Request: + return httpx.Request("GET", "https://verification.didit.me/v3/x/") + + +# Errors ------------------------------------------------------------------------------ + + +@pytest.mark.parametrize( + ("status", "cls"), + [ + (400, didit.BadRequestError), + (401, didit.AuthenticationError), + (403, didit.PermissionDeniedError), + (404, didit.NotFoundError), + (409, didit.ConflictError), + (422, didit.UnprocessableEntityError), + (429, didit.RateLimitError), + (500, didit.InternalServerError), + (503, didit.InternalServerError), + (418, didit.APIStatusError), + ], +) +def test_status_errors(status: int, cls: type[didit.APIStatusError]) -> None: + api = MockAPI().add( + httpx.Response(status, json={"detail": "nope"}, headers={"x-request-id": "req_9"}) + ) + client = api.client(max_retries=0) + with pytest.raises(cls) as info: + client.sessions.retrieve_decision("s") + err = info.value + assert type(err) is cls + assert isinstance(err, didit.APIStatusError) + assert isinstance(err, didit.DiditError) + assert err.status == status == err.status_code + assert err.body == {"detail": "nope"} + assert err.request_id == "req_9" + assert err.headers["x-request-id"] == "req_9" + assert err.message == f"HTTP {status}: nope" + assert str(err) == f"HTTP {status}: nope (request id req_9)" + + +@pytest.mark.parametrize( + ("body", "message"), + [ + ({"error": "Invalid workflow"}, "HTTP 400: Invalid workflow"), + ({"message": "bad"}, "HTTP 400: bad"), + ( + {"workflow_id": ["This field is required."], "language": "unknown"}, + "HTTP 400: workflow_id: This field is required.; language: unknown", + ), + (["one", "two"], "HTTP 400: one; two"), + ({}, "HTTP 400"), + ], +) +def test_error_messages(body: Any, message: str) -> None: + api = MockAPI().add(httpx.Response(400, json=body)) + with pytest.raises(didit.BadRequestError) as info: + api.client().sessions.create(workflow_id="w") + assert info.value.message == message + assert info.value.request_id is None + + +def test_non_json_error_body() -> None: + api = MockAPI().add(httpx.Response(502, text="Bad gateway")) + with pytest.raises(didit.InternalServerError) as info: + api.client(max_retries=0).sessions.retrieve_decision("s") + assert info.value.body == "Bad gateway" + empty = MockAPI().add(httpx.Response(404)) + with pytest.raises(didit.NotFoundError) as info2: + empty.client().sessions.retrieve_decision("s") + assert info2.value.body is None + + +def test_error_messages_never_contain_the_api_key() -> None: + api = MockAPI().add(httpx.Response(403, json={"detail": f"Key {API_KEY} is revoked"})) + with pytest.raises(didit.PermissionDeniedError) as info: + api.client().sessions.retrieve_decision("s") + assert API_KEY not in str(info.value) + assert API_KEY not in repr(info.value) + assert "[redacted]" in info.value.message + + +def test_html_on_success_is_a_response_validation_error() -> None: + api = MockAPI().add( + httpx.Response(200, text="", headers={"content-type": "text/html"}) + ) + with pytest.raises(didit.APIResponseValidationError) as info: + api.client().sessions.retrieve_decision("s") + assert info.value.status == 200 + + +def test_connection_and_timeout_errors() -> None: + api = MockAPI().add(httpx.ConnectError("refused")) + with pytest.raises(didit.APIConnectionError) as info: + api.client(max_retries=0).sessions.retrieve_decision("s") + assert not isinstance(info.value, didit.APITimeoutError) + assert info.value.request.url.path == "/v3/session/s/decision/" + api = MockAPI().add(httpx.ReadTimeout("slow")) + with pytest.raises(didit.APITimeoutError): + api.client(max_retries=0).sessions.retrieve_decision("s") + + +class _BrokenBody(httpx.SyncByteStream, httpx.AsyncByteStream): + """A body that starts arriving and then stalls or drops.""" + + def __init__(self, error: Exception) -> None: + self.error = error + + def __iter__(self) -> Iterator[bytes]: + yield b'{"session_id": "' + raise self.error + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield b'{"session_id": "' + raise self.error + + +def _broken(status: int, error: Exception) -> Any: + return lambda _request: httpx.Response(status, stream=_BrokenBody(error)) + + +@pytest.mark.parametrize( + ("error", "cls"), + [ + (httpx.ReadTimeout("stalled"), didit.APITimeoutError), + (httpx.RemoteProtocolError("peer closed"), didit.APIConnectionError), + (httpx.ReadError("reset"), didit.APIConnectionError), + ], +) +def test_errors_while_reading_the_body_are_sdk_errors_and_retried_for_get( + error: Exception, cls: type[didit.APIConnectionError] +) -> None: + api = MockAPI().add(_broken(200, error), _broken(200, error), _broken(200, error)) + with pytest.raises(cls) as info: + api.client().sessions.retrieve_decision("s") + assert len(api.requests) == 3 + assert info.value.request.headers["x-api-key"] == "[redacted]" + # A GET whose body broke is retried and can then succeed. + api = MockAPI().add(_broken(200, error)).json(200, {"session_id": "s"}) + assert api.client().sessions.retrieve_decision("s").session_id == "s" + # An error response whose body broke is a connection error too. + api = MockAPI().add(_broken(404, error)) + with pytest.raises(cls): + api.client(max_retries=0).sessions.retrieve_decision("s") + + +def test_a_post_whose_body_broke_is_not_retried() -> None: + api = MockAPI().add(_broken(201, httpx.ReadTimeout("stalled"))) + with pytest.raises(didit.APITimeoutError): + api.client().sessions.create(workflow_id="w-1") + assert len(api.requests) == 1 + + +def test_async_errors_while_reading_the_body() -> None: + async def run() -> None: + api = MockAPI().add(*[_broken(200, httpx.ReadTimeout("stalled"))] * 3) + with pytest.raises(didit.APITimeoutError): + await api.async_client().sessions.retrieve_decision("s") + assert len(api.requests) == 3 + api = MockAPI().add(_broken(201, httpx.RemoteProtocolError("dropped"))) + with pytest.raises(didit.APIConnectionError): + await api.async_client().sessions.create(workflow_id="w-1") + assert len(api.requests) == 1 + + asyncio.run(run()) + + +def test_streamed_body_errors_are_sdk_errors() -> None: + api = MockAPI().add(_broken(200, httpx.ReadError("reset"))) + client = api.client() + with ( + client.sessions.with_streaming_response.generate_pdf("s") as response, + pytest.raises(didit.APIConnectionError), + ): + b"".join(response.iter_bytes()) + + async def run() -> None: + api = MockAPI().add(_broken(200, httpx.ReadTimeout("stalled"))) + async with api.async_client().sessions.with_streaming_response.generate_pdf( + "s" + ) as response: + with pytest.raises(didit.APITimeoutError): + await response.read() + + asyncio.run(run()) + + +def test_errors_keep_no_credentials_on_their_request() -> None: + api = MockAPI().add(httpx.Response(400, json={"detail": "bad"})) + with pytest.raises(didit.BadRequestError) as info: + api.client().sessions.retrieve_decision("s") + assert info.value.response.request.headers["x-api-key"] == "[redacted]" + assert API_KEY not in repr(info.value.response.request.headers) + api = MockAPI().add(httpx.ConnectError("refused")) + with pytest.raises(didit.APIConnectionError) as conn: + api.client(max_retries=0).sessions.retrieve_decision("s") + assert conn.value.request.headers["x-api-key"] == "[redacted]" + api = MockAPI().add(httpx.Response(200, text="", headers={"content-type": "text/html"})) + with pytest.raises(didit.APIResponseValidationError) as html: + api.client().sessions.retrieve_decision("s") + assert html.value.response.request.headers["x-api-key"] == "[redacted]" + api = MockAPI().add(httpx.Response(200, text="")) + with pytest.raises(didit.APIResponseValidationError) as page: + api.client().businesses.list() + assert page.value.response.request.headers["x-api-key"] == "[redacted]" + api = MockAPI().add(_broken(200, httpx.ReadTimeout("stalled"))) + with ( + api.client().sessions.with_streaming_response.generate_pdf("s") as response, + pytest.raises(didit.APITimeoutError) as streamed, + ): + response.read() + assert streamed.value.request.headers["x-api-key"] == "[redacted]" + + +# Retry policy --------------------------------------------------------------------- + + +@pytest.mark.parametrize("method", ["GET", "HEAD", "PUT", "DELETE"]) +@pytest.mark.parametrize("status", [408, 429, 500, 502, 503, 504]) +def test_idempotent_methods_retry_on_retryable_statuses(method: str, status: int) -> None: + assert should_retry_status(method, status) + + +@pytest.mark.parametrize("method", ["POST", "PATCH"]) +def test_post_and_patch_retry_only_on_429(method: str) -> None: + assert should_retry_status(method, 429) + for status in (408, 500, 502, 503, 504): + assert not should_retry_status(method, status) + + +@pytest.mark.parametrize("status", [400, 401, 403, 404, 409, 422]) +def test_client_errors_never_retry(status: int) -> None: + assert not should_retry_status("GET", status) + + +def test_exception_policy() -> None: + request = _request() + for exc in ( + httpx.ConnectError("x", request=request), + httpx.ConnectTimeout("x", request=request), + httpx.PoolTimeout("x", request=request), + ): + assert should_retry_exception("POST", exc) + assert should_retry_exception("GET", exc) + for late in ( + httpx.ReadTimeout("x", request=request), + httpx.WriteTimeout("x", request=request), + httpx.ReadError("x", request=request), + httpx.RemoteProtocolError("x", request=request), + ): + assert not should_retry_exception("POST", late) + assert not should_retry_exception("PATCH", late) + assert should_retry_exception("GET", late) + assert not should_retry_exception("GET", httpx.UnsupportedProtocol("x", request=request)) + + +def test_get_retries_5xx_then_succeeds(no_sleep: list[float]) -> None: + api = MockAPI().add(httpx.Response(503), httpx.Response(500)).json(200, {"session_id": "s"}) + result = api.client().sessions.retrieve_decision("s") + assert result.session_id == "s" + assert len(api.requests) == 3 + assert len(no_sleep) == 2 + assert 0 <= no_sleep[0] <= 0.5 + assert 0 <= no_sleep[1] <= 1.0 + + +def test_retries_stop_at_max_retries() -> None: + api = MockAPI().add( + httpx.Response(500), httpx.Response(500), httpx.Response(500), httpx.Response(500) + ) + with pytest.raises(didit.InternalServerError): + api.client(max_retries=2).sessions.retrieve_decision("s") + assert len(api.requests) == 3 + + +def test_post_is_not_retried_after_a_5xx() -> None: + api = MockAPI().add(httpx.Response(500)).json(201, SESSION) + with pytest.raises(didit.InternalServerError): + api.client().sessions.create(workflow_id="w") + assert len(api.requests) == 1 + + +def test_post_is_not_retried_after_a_read_timeout() -> None: + api = MockAPI().add(httpx.ReadTimeout("slow")).json(201, SESSION) + with pytest.raises(didit.APITimeoutError): + api.client().sessions.create(workflow_id="w") + assert len(api.requests) == 1 + + +def test_post_is_retried_when_the_connection_failed_before_sending() -> None: + api = ( + MockAPI() + .add(httpx.ConnectError("refused"), httpx.ConnectTimeout("slow")) + .json(201, SESSION) + ) + session = api.client().sessions.create(workflow_id="w") + assert session.session_id == SESSION["session_id"] + assert len(api.requests) == 3 + + +def test_post_is_retried_on_429_honouring_retry_after(no_sleep: list[float]) -> None: + api = MockAPI().add(httpx.Response(429, headers={"Retry-After": "7"})).json(201, SESSION) + api.client().sessions.create(workflow_id="w") + assert len(api.requests) == 2 + assert no_sleep == [7.0] + + +def test_get_retries_timeouts_and_dropped_connections() -> None: + api = ( + MockAPI().add(httpx.ReadTimeout("slow"), httpx.RemoteProtocolError("dropped")).json(200, {}) + ) + api.client().sessions.retrieve_decision("s") + assert len(api.requests) == 3 + + +def test_max_retries_zero_and_per_call_override() -> None: + api = MockAPI().add(httpx.Response(503)) + with pytest.raises(didit.InternalServerError): + api.client(max_retries=0).sessions.retrieve_decision("s") + assert len(api.requests) == 1 + api = MockAPI().add(httpx.Response(503), httpx.Response(503)).json(200, {}) + client = api.client(max_retries=0) + client.with_options(max_retries=2).sessions.retrieve_decision("s") + assert len(api.requests) == 3 + + +def test_retry_delays_are_capped(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("didit._base_client.random.uniform", lambda low, high: high) + assert [retry_delay(n, None) for n in range(6)] == [0.5, 1.0, 2.0, 4.0, 8.0, 8.0] + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("3", 3.0), + ("0.5", 0.5), + ("120", 60.0), + ("-4", 0.0), + ("soon", None), + ("", None), + ("nan", None), + ], +) +def test_retry_after_seconds(value: str, expected: float | None) -> None: + assert parse_retry_after(httpx.Headers({"Retry-After": value})) == expected + + +def test_retry_after_http_date() -> None: + now = time.time() + date = email.utils.formatdate(now + 10, usegmt=True) + parsed = parse_retry_after(httpx.Headers({"Retry-After": date}), now=now) + assert parsed is not None + assert 9 <= parsed <= 10 + far = email.utils.formatdate(now + 3600, usegmt=True) + assert parse_retry_after(httpx.Headers({"Retry-After": far}), now=now) == 60.0 + assert parse_retry_after(httpx.Headers({})) is None + + +def test_retry_after_is_used_for_get(no_sleep: list[float]) -> None: + api = MockAPI().add(httpx.Response(503, headers={"Retry-After": "2"})).json(200, {}) + api.client().sessions.retrieve_decision("s") + assert no_sleep == [2.0] + + +def test_async_retry_policy(no_sleep: list[float]) -> None: + api = MockAPI().add(httpx.Response(502), httpx.ConnectError("x")).json(200, {"session_id": "s"}) + post = MockAPI().add(httpx.Response(500)) + + async def run() -> None: + client = api.async_client() + result = await client.sessions.retrieve_decision("s") + assert result.session_id == "s" + with pytest.raises(didit.InternalServerError): + await post.async_client().sessions.create(workflow_id="w") + with pytest.raises(didit.APITimeoutError): + await ( + MockAPI() + .add(httpx.ReadTimeout("x")) + .async_client(max_retries=0) + .sessions.retrieve_decision("s") + ) + with pytest.raises(didit.APIConnectionError): + await ( + MockAPI() + .add(httpx.ConnectError("x")) + .async_client(max_retries=0) + .sessions.retrieve_decision("s") + ) + with pytest.raises(didit.NotFoundError): + await ( + MockAPI() + .add(httpx.Response(404, json={"detail": "gone"})) + .async_client() + .sessions.retrieve_decision("s") + ) + timeout_then_ok = MockAPI().add(httpx.ReadTimeout("x")).json(200, {}) + await timeout_then_ok.async_client().sessions.retrieve_decision("s") + assert len(timeout_then_ok.requests) == 2 + + asyncio.run(run()) + assert len(api.requests) == 3 + assert len(post.requests) == 1 + assert len(no_sleep) == 3 diff --git a/tests/test_live.py b/tests/test_live.py new file mode 100644 index 0000000..768666f --- /dev/null +++ b/tests/test_live.py @@ -0,0 +1,50 @@ +"""Live smoke tests against a Didit sandbox application. + +They run only with ``DIDIT_SANDBOX_API_KEY`` set (``uv run pytest -m live``), never in +the default CI, and create nothing that costs money beyond sandbox sessions. +""" + +from __future__ import annotations + +import os + +import pytest + +import didit + +API_KEY = os.environ.get("DIDIT_SANDBOX_API_KEY") +WORKFLOW_ID = os.environ.get("DIDIT_SANDBOX_WORKFLOW_ID") + +pytestmark = [ + pytest.mark.live, + pytest.mark.skipif(not API_KEY, reason="DIDIT_SANDBOX_API_KEY is not set"), +] + + +@pytest.fixture +def client() -> didit.Didit: + return didit.Didit(api_key=API_KEY, base_url=os.environ.get("DIDIT_SANDBOX_BASE_URL")) + + +def test_list_workflows(client: didit.Didit) -> None: + page = client.workflows.list(limit=5) + assert page.count is None or page.count >= 0 + + +def test_list_sessions_first_page(client: didit.Didit) -> None: + page = client.sessions.list(limit=2) + assert len(page.results) <= 2 + + +@pytest.mark.skipif(not WORKFLOW_ID, reason="DIDIT_SANDBOX_WORKFLOW_ID is not set") +def test_create_session_and_read_decision(client: didit.Didit) -> None: + assert WORKFLOW_ID is not None + session = client.sessions.create(workflow_id=WORKFLOW_ID, vendor_data="sdk-python-smoke") + assert session.url + decision = client.sessions.retrieve_decision(session.session_id) + assert decision.session_id == session.session_id + + +def test_unknown_session_is_not_found(client: didit.Didit) -> None: + with pytest.raises(didit.NotFoundError): + client.sessions.retrieve_decision("00000000-0000-4000-8000-000000000000") diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 0000000..02f387b --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,129 @@ +"""Response models: unknown fields kept, contract mismatches tolerated, absent versus null (DESIGN 5.1).""" + +from __future__ import annotations + +import datetime as dt +from typing import Any + +import pydantic +import pytest + +import didit +from didit._models import BaseModel, construct_type, validate_response +from didit.types import SessionsCreateResponse, TransactionDetail +from tests.mock_api import SESSION, MockAPI + + +class Inner(BaseModel): + name: str + when: dt.datetime | None = None + + +class Outer(BaseModel): + id: int + inner: Inner | None = None + items: list[Inner] | None = None + tags: dict[str, Inner] | None = None + either: Inner | int | None = None + + +def test_unknown_fields_are_kept() -> None: + api = MockAPI().json(201, {**SESSION, "brand_new_field": {"x": 1}}) + session = api.client().sessions.create(workflow_id="w") + assert isinstance(session, SessionsCreateResponse) + assert session.model_extra == {"brand_new_field": {"x": 1}} + assert session.to_dict()["brand_new_field"] == {"x": 1} + assert '"brand_new_field"' in session.to_json() + + +def test_absent_and_null_are_distinguishable() -> None: + model = validate_response(Outer, {"id": 1, "inner": None}) + assert model.inner is None + assert model.items is None + assert "inner" in model.model_fields_set + assert "items" not in model.model_fields_set + assert model.to_dict() == {"id": 1, "inner": None} + + +def test_dates_are_parsed() -> None: + model = validate_response( + Outer, {"id": 1, "inner": {"name": "a", "when": "2026-01-02T03:04:05Z"}} + ) + assert model.inner.when == dt.datetime(2026, 1, 2, 3, 4, 5, tzinfo=dt.timezone.utc) + + +def test_contract_mismatch_never_raises() -> None: + data: dict[str, Any] = { + "id": "not-an-int", + "inner": {"name": 5, "when": "yesterday"}, + "items": [{"name": "ok"}, {"bad": True}, "junk"], + "tags": {"a": {"name": "x"}}, + "either": {"name": "n"}, + "extra": 1, + } + model = validate_response(Outer, data) + assert isinstance(model, Outer) + loose: Any = model + assert loose.id == "not-an-int" + assert isinstance(model.inner, Inner) + assert loose.inner.name == 5 + assert loose.inner.when == "yesterday" + assert model.items is not None + assert isinstance(model.items[0], Inner) + assert loose.items[2] == "junk" + assert model.tags is not None + assert isinstance(model.tags["a"], Inner) + assert isinstance(model.either, Inner) + assert model.model_extra == {"extra": 1} + + +def test_construct_type_edge_cases() -> None: + assert construct_type(int, None) is None + assert construct_type(Inner, "not a dict") == "not a dict" + assert construct_type(list[int], "x") == "x" + assert construct_type(dict[str, int], 3) == 3 + assert construct_type(dt.date, "2026-01-01") == dt.date(2026, 1, 1) + assert construct_type(dt.date, "bad") == "bad" + assert construct_type(Inner | int, "str") == "str" + assert construct_type(object, {"a": 1}) == {"a": 1} + + +def test_real_model_with_wrong_shape() -> None: + api = MockAPI().json(200, {"uuid": 1234, "status": {"weird": True}, "surprise": [1]}) + transaction = api.client().transactions.retrieve("t-1") + assert isinstance(transaction, TransactionDetail) + loose: Any = transaction + assert loose.uuid == 1234 + assert loose.status == {"weird": True} + assert transaction.model_extra == {"surprise": [1]} + + +def test_validate_response_passthrough() -> None: + assert validate_response(object, {"a": 1}) == {"a": 1} + assert validate_response(list[int], ["1", 2]) == [1, 2] + assert validate_response(list[int], ["x"]) == ["x"] + + +def test_public_base_model() -> None: + assert issubclass(SessionsCreateResponse, didit.BaseModel) + assert issubclass(didit.BaseModel, pydantic.BaseModel) + + +def test_missing_required_fields_read_none(recwarn: pytest.WarningsRecorder) -> None: + from didit.types import WalletScreeningResult + + data = {"risk_score": "high", "brand_new": 1} + result = validate_response(WalletScreeningResult, data) + assert isinstance(result, WalletScreeningResult) + provider: object = result.provider # absent: reads None instead of raising AttributeError + assert provider is None + assert "provider" not in result.model_fields_set + risk_score: object = result.risk_score + assert risk_score == "high" + assert result.to_dict() == data + assert '"risk_score": "high"' in result.to_json() + assert not [w for w in recwarn if issubclass(w.category, UserWarning)] + nested = validate_response(Outer, {"id": 1, "inner": {"when": "not a date"}}) + assert nested.inner is not None + name: object = nested.inner.name + assert name is None diff --git a/tests/test_pagination.py b/tests/test_pagination.py new file mode 100644 index 0000000..4eaf2df --- /dev/null +++ b/tests/test_pagination.py @@ -0,0 +1,238 @@ +"""Auto-pagination and the next-link origin check (DESIGN 6 and 8).""" + +from __future__ import annotations + +import asyncio +from typing import Any + +import httpx +import pytest + +import didit +from didit.types import BusinessListItem +from tests.mock_api import API_KEY, MockAPI + +BASE = "https://verification.didit.me/v3/businesses/" + + +def _page(ids: list[str], next_url: str | None, count: int = 5, **extra: Any) -> dict[str, Any]: + return { + "count": count, + "next": next_url, + "previous": None, + "results": [{"didit_internal_id": i, "vendor_data": f"b-{i}"} for i in ids], + **extra, + } + + +def _three_pages() -> MockAPI: + return ( + MockAPI() + .json(200, _page(["1", "2"], f"{BASE}?limit=2&offset=2")) + .json(200, _page(["3", "4"], f"{BASE}?limit=2&offset=4")) + .json(200, _page(["5"], None)) + ) + + +def test_page_fields_and_iteration() -> None: + api = _three_pages() + page = api.client().businesses.list(limit=2) + assert isinstance(page, didit.SyncPage) + assert page.count == 5 + assert page.next == f"{BASE}?limit=2&offset=2" + assert page.previous is None + assert [b.didit_internal_id for b in page.results] == ["1", "2"] + assert all(isinstance(b, BusinessListItem) for b in page.results) + assert page.response is not None + assert page.has_next_page() + assert "count=5" in repr(page) + assert len(api.requests) == 1 + assert api.requests[0].url.params["limit"] == "2" + + assert [b.didit_internal_id for b in page] == ["1", "2", "3", "4", "5"] + assert len(api.requests) == 3 + assert api.requests[1].url.params["offset"] == "2" + assert api.requests[2].url.params["offset"] == "4" + for request in api.requests: + assert request.headers["x-api-key"] == API_KEY + + +def test_iter_pages_and_get_next_page() -> None: + api = _three_pages() + pages = list(api.client().businesses.list(limit=2).iter_pages()) + assert [len(p.results) for p in pages] == [2, 2, 1] + assert not pages[-1].has_next_page() + with pytest.raises(didit.DiditError, match="last page"): + pages[-1].get_next_page() + + +def test_next_link_on_another_origin_is_refused() -> None: + for evil in ( + "https://evil.example.com/v3/businesses/?offset=2", + "http://verification.didit.me/v3/businesses/?offset=2", + "https://verification.didit.me:8443/v3/businesses/?offset=2", + ): + api = MockAPI().json(200, _page(["1"], evil)) + page = api.client().businesses.list() + with pytest.raises(didit.DiditError, match="another origin"): + page.get_next_page() + with pytest.raises(didit.DiditError): + list(page) + assert len(api.requests) == 1 + + +def test_default_port_counts_as_the_same_origin() -> None: + api = ( + MockAPI() + .json(200, _page(["1"], "https://verification.didit.me:443/v3/businesses/?offset=1")) + .json(200, _page(["2"], None)) + ) + assert [b.didit_internal_id for b in api.client().businesses.list()] == ["1", "2"] + + +def test_extra_list_fields_are_kept_on_the_response() -> None: + api = MockAPI().json( + 200, + {"count": 1, "next": None, "previous": None, "results": [], "total_transactions": 42}, + ) + page = api.client().transactions.list() + assert page.response is not None + assert page.response.model_dump()["total_transactions"] == 42 + + +def test_raw_response_of_a_list() -> None: + api = MockAPI().add( + httpx.Response(200, json=_page(["1"], None), headers={"x-request-id": "req_1"}) + ) + raw = api.client().businesses.with_raw_response.list() + assert raw.status_code == 200 + assert raw.request_id == "req_1" + page = raw.parse() + assert isinstance(page, didit.SyncPage) + assert [b.didit_internal_id for b in page] == ["1"] + + +def test_empty_page_stops() -> None: + api = MockAPI().json(200, _page([], f"{BASE}?offset=0", count=0)) + assert list(api.client().businesses.list()) == [] + assert len(api.requests) == 1 + + +def test_async_pagination() -> None: + api = _three_pages() + + async def run() -> None: + client = api.async_client() + page = await client.businesses.list(limit=2) + assert isinstance(page, didit.AsyncPage) + assert [b.didit_internal_id for b in page.results] == ["1", "2"] + seen = [b.didit_internal_id async for b in page] + assert seen == ["1", "2", "3", "4", "5"] + + asyncio.run(run()) + assert len(api.requests) == 3 + + api = _three_pages() + + async def run_direct() -> list[str | None]: + client = api.async_client() + return [b.didit_internal_id async for b in client.businesses.list(limit=2)] + + assert asyncio.run(run_direct()) == ["1", "2", "3", "4", "5"] + + api = _three_pages() + api.json(200, _page(["3", "4"], f"{BASE}?limit=2&offset=4")).json(200, _page(["5"], None)) + api.json(200, _page(["1"], None)) + + async def run_pages() -> list[int]: + client = api.async_client() + first = await client.businesses.list(limit=2) + sizes = [len(p.results) async for p in first.iter_pages()] + last = await (await first.get_next_page()).get_next_page() + with pytest.raises(didit.DiditError): + await last.get_next_page() + raw = await client.businesses.with_raw_response.list() + parsed = await raw.parse() + assert isinstance(parsed, didit.AsyncPage) + return sizes + + assert asyncio.run(run_pages()) == [2, 2, 1] + + +def test_async_next_link_on_another_origin_is_refused() -> None: + api = MockAPI().json(200, _page(["1"], "https://evil.example.com/v3/businesses/")) + + async def run() -> None: + page = await api.async_client().businesses.list() + with pytest.raises(didit.DiditError, match="another origin"): + await page.get_next_page() + + asyncio.run(run()) + + +def test_not_a_page() -> None: + api = MockAPI().json(200, []) + with pytest.raises(didit.DiditError): + api.client().businesses.list() + + +def _looping_api() -> MockAPI: + loop = f"{BASE}?limit=1&offset=1" + return MockAPI().add(*[lambda _r: httpx.Response(200, json=_page(["1"], loop))] * 5) + + +def test_a_repeated_next_link_stops_instead_of_looping() -> None: + api = _looping_api() + with pytest.raises(didit.DiditError, match="twice"): + list(api.client().businesses.list(limit=1)) + assert len(api.requests) == 2 + + async def run() -> None: + with pytest.raises(didit.DiditError, match="twice"): + async for _ in api.async_client().businesses.list(limit=1): + pass + + api = _looping_api() + asyncio.run(run()) + assert len(api.requests) == 2 + + +def test_a_relative_next_link_is_resolved_against_the_page_url() -> None: + api = ( + MockAPI() + .json(200, _page(["1"], "/v3/businesses/?limit=1&offset=1")) + .json(200, _page(["2"], "?limit=1&offset=2")) + .json(200, _page(["3"], None)) + ) + ids = [b.didit_internal_id for b in api.client().businesses.list(limit=1)] + assert ids == ["1", "2", "3"] + assert [str(r.url) for r in api.requests[1:]] == [ + f"{BASE}?limit=1&offset=1", + f"{BASE}?limit=1&offset=2", + ] + assert all(r.headers["x-api-key"] == API_KEY for r in api.requests) + + +def test_a_list_that_is_not_json_raises_a_validation_error() -> None: + for response in ( + httpx.Response(200, text="maintenance", headers={"content-type": "text/html"}), + httpx.Response(204), + ): + api = MockAPI().add(response) + with pytest.raises(didit.APIResponseValidationError, match="paginated JSON object"): + api.client().businesses.list() + + async def run() -> None: + api = MockAPI().add(httpx.Response(200, text="not json")) + with pytest.raises(didit.APIResponseValidationError): + await api.async_client().businesses.list() + + asyncio.run(run()) + + +def test_an_invalid_next_link_is_refused() -> None: + api = MockAPI().json(200, _page(["1"], "https://[::1")) + page = api.client().businesses.list() + with pytest.raises(didit.DiditError, match="invalid pagination link"): + page.get_next_page() + assert len(api.requests) == 1 diff --git a/tests/test_types.py b/tests/test_types.py new file mode 100644 index 0000000..2cde5ee --- /dev/null +++ b/tests/test_types.py @@ -0,0 +1,54 @@ +"""Every generated type imports and builds (models are built lazily, so check them all).""" + +from __future__ import annotations + +import importlib +import pkgutil +import subprocess +import sys + +import pydantic +import pytest + +import didit +import didit.resources +import didit.types + + +def test_every_exported_type_resolves_and_builds() -> None: + assert len(didit.types.__all__) > 800 + models = 0 + for name in didit.types.__all__: + value = getattr(didit.types, name) + if isinstance(value, type) and issubclass(value, pydantic.BaseModel): + # Response models: building the schema resolves every annotation. + value.model_rebuild(force=True) + pydantic.TypeAdapter(value).json_schema() + models += 1 + else: + # Request TypedDicts and aliases only need to be importable. + assert value is not None + assert models > 500 + assert "SessionsCreateResponse" in dir(didit.types) + with pytest.raises(AttributeError): + getattr(didit.types, "NoSuchType") # noqa: B009 + + +def test_every_generated_module_imports() -> None: + for package in (didit.types, didit.resources): + for info in pkgutil.walk_packages(package.__path__, package.__name__ + "."): + importlib.import_module(info.name) + + +def test_import_is_light() -> None: + code = ( + "import sys, didit; " + "loaded = [m for m in sys.modules if m.startswith(('didit.resources.', 'didit.types.'))]; " + "print(len(loaded), 'cryptography' in sys.modules)" + ) + out = subprocess.run( + [sys.executable, "-c", code], capture_output=True, text=True, check=True + ).stdout.split() + # Only the decision model the webhook events reuse is loaded up front. + assert int(out[0]) < 10 + assert out[1] == "False" diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py new file mode 100644 index 0000000..56013c2 --- /dev/null +++ b/tests/test_webhooks.py @@ -0,0 +1,512 @@ +"""Webhook verification: every backend vector (openapi/webhook-vectors.json) and the edge cases.""" + +from __future__ import annotations + +import hashlib +import hmac +import json +import pathlib +from typing import Any + +import pytest + +import didit +from didit import ConfigurationError, Didit, WebhookVerificationError, webhooks +from didit.webhooks import ( + SessionStatusUpdatedEvent, + TransactionStatusUpdatedEvent, + TravelRuleStatusUpdatedEvent, + UnknownEvent, + UserStatusUpdatedEvent, + canonical_json, +) + +VECTORS_FILE = pathlib.Path(__file__).resolve().parents[1] / "openapi" / "webhook-vectors.json" +VECTORS: list[dict[str, Any]] = json.loads(VECTORS_FILE.read_text("utf-8"))["vectors"] +SECRET = "whsec_test_unit_secret_0001" +PREVIOUS = "whsec_test_unit_secret_0000" +NOW = 1_760_702_400 + + +@pytest.fixture +def frozen_time(monkeypatch: pytest.MonkeyPatch) -> list[float]: + clock = [float(NOW)] + monkeypatch.setattr("didit.webhooks._verify.time.time", lambda: clock[0]) + return clock + + +def _sign(secret: str, message: bytes) -> str: + return hmac.new(secret.encode(), message, hashlib.sha256).hexdigest() + + +def _delivery(payload: dict[str, Any], secret: str = SECRET) -> tuple[bytes, dict[str, str]]: + """A delivery signed exactly like service-didit-verification does it.""" + body = json.dumps( + webhooks._canonical.shorten_floats(payload), sort_keys=True, separators=(",", ":") + ) + canonical = canonical_json(json.loads(body)) + simple = ":".join( + str(payload.get(k, "")) for k in ("timestamp", "session_id", "status", "webhook_type") + ) + headers = { + "X-Signature": _sign(secret, body.encode()), + "X-Signature-V2": _sign(secret, canonical.encode()), + "X-Signature-Simple": _sign(secret, simple.encode()), + "X-Timestamp": str(payload["timestamp"]), + } + return body.encode(), headers + + +def _payload(**extra: Any) -> dict[str, Any]: + base: dict[str, Any] = { + "application_id": "0b5e8d2c-1f4a-4c3b-9d6e-2a7f8c1b3e5d", + "created_at": NOW, + "environment": "sandbox", + "event_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "session_id": "11111111-2222-3333-4444-555555555555", + "status": "Approved", + "timestamp": NOW, + "webhook_type": "status.updated", + } + base.update(extra) + return base + + +# Backend vectors ------------------------------------------------------------------ + + +@pytest.mark.parametrize("vector", VECTORS, ids=[v["name"] for v in VECTORS]) +def test_vector(vector: dict[str, Any], monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr("didit.webhooks._verify.time.time", lambda: float(vector["now"])) + body: str = vector["body"] + headers: dict[str, str] = vector["headers"] + secret: str = vector["secret"] + expect: str = vector["expect"] + + if expect == "invalid_signature" and vector["name"] == "tampered_body": + # The canonical string was recorded before the body was altered. + assert canonical_json(json.loads(body)) != vector["canonical_v2"] + else: + assert canonical_json(json.loads(body)) == vector["canonical_v2"] + + if expect == "valid": + event = webhooks.verify(body.encode(), headers, secret) + assert event.body_authenticated + assert event.webhook_type == json.loads(body)["webhook_type"] + if vector["previous_secret"]: + # A receiver that only has the previous secret still verifies during a rotation. + assert webhooks.verify(body, headers, vector["previous_secret"]).body_authenticated + assert webhooks.verify( + body, headers, [vector["previous_secret"], secret] + ).body_authenticated + # X-Signature-V2 alone also verifies, from the raw body or the parsed payload. + v2_only = { + k: v + for k, v in headers.items() + if k in ("X-Signature-V2", "X-Signature-V2-Previous", "X-Timestamp") + } + assert webhooks.verify(body, v2_only, secret).verified_by == "signature_v2" + assert ( + webhooks.verify_parsed(json.loads(body), v2_only, secret).verified_by == "signature_v2" + ) + elif expect == "valid_only_with_allow_simple": + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, headers, secret) + assert info.value.reason == "invalid_signature" + event = webhooks.verify(body, headers, secret, allow_simple=True) + assert event.verified_by == "signature_simple" + assert not event.body_authenticated + else: + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body.encode(), headers, secret) + assert info.value.reason == expect + + +def test_vectors_cover_every_outcome() -> None: + outcomes = {v["expect"] for v in VECTORS} + assert outcomes == { + "valid", + "valid_only_with_allow_simple", + "invalid_signature", + "missing_signature", + "timestamp_out_of_tolerance", + "timestamp_mismatch", + } + assert len(VECTORS) == 25 + + +# Canonical JSON ------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ({"b": 1, "a": 2}, '{"a":2,"b":1}'), + ({"score": 87.0}, '{"score":87}'), + ({"x": 1e16}, '{"x":10000000000000000}'), + ({"x": 1e-05}, '{"x":1e-05}'), + ({"x": 1.5e-07}, '{"x":1.5e-07}'), + ({"x": 0.1}, '{"x":0.1}'), + ({"x": -2.5}, '{"x":-2.5}'), + ({"x": 9007199254740993}, '{"x":9007199254740993}'), + ({"x": [1.0, 2.5, None, True, False]}, '{"x":[1,2.5,null,true,false]}'), + ({"s": "Jos\u00e9 \U0001f600"}, '{"s":"Jos\u00e9 \U0001f600"}'), + ({"s": "a\u2028b\u2029c\u007f/"}, '{"s":"a\u2028b\u2029c\u007f/"}'), + ({"s": 'q"\\\n\r\t\b\f\u0007\u001f'}, '{"s":"q\\"\\\\\\n\\r\\t\\b\\f\\u0007\\u001f"}'), + # Keys sort by code point, not by UTF-16 code unit (U+FB00 < U+1F600). + ({"\U0001f600": 1, "\ufb00": 2, "\u00e9": 3}, '{"\u00e9":3,"\ufb00":2,"\U0001f600":1}'), + ], +) +def test_canonical_json(value: object, expected: str) -> None: + assert canonical_json(value) == expected + + +def test_canonical_json_rejects_non_finite() -> None: + with pytest.raises(WebhookVerificationError) as info: + canonical_json({"x": float("nan")}) + assert info.value.reason == "invalid_payload" + + +# Verification rules --------------------------------------------------------------- + + +def test_valid_delivery_returns_typed_event(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload(vendor_data="user-1", workflow_version=3)) + event = webhooks.verify(body, headers, SECRET) + assert isinstance(event, SessionStatusUpdatedEvent) + assert event.verified_by == "signature" + assert event.status == "Approved" + assert event.vendor_data == "user-1" + assert event.workflow_version == 3 + assert event.environment == "sandbox" + + +def test_reencoded_body_still_verifies_with_v2(frozen_time: list[float]) -> None: + payload = _payload(metadata={"name": "Jos\u00e9", "score": 87.0}) + body, headers = _delivery(payload) + # Middleware that re-encodes the JSON (different key order, unescaped Unicode, 87.0). + reencoded = json.dumps(json.loads(body), ensure_ascii=False, indent=2).replace("87", "87.0") + event = webhooks.verify(reencoded.encode(), headers, SECRET) + assert event.verified_by == "signature_v2" + + +def test_headers_are_case_insensitive_and_accept_pairs(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + lower = {k.lower(): v for k, v in headers.items()} + assert webhooks.verify(body, lower, SECRET).body_authenticated + assert webhooks.verify(body, list(headers.items()), SECRET).body_authenticated + wsgi = {"HTTP_" + k.upper().replace("-", "_"): v for k, v in headers.items()} + assert webhooks.verify(body, wsgi, SECRET).body_authenticated + + +def test_secret_list_for_rotation(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload(), secret=PREVIOUS) + with pytest.raises(WebhookVerificationError): + webhooks.verify(body, headers, SECRET) + assert webhooks.verify(body, headers, [SECRET, PREVIOUS]).body_authenticated + + +@pytest.mark.parametrize("secret", ["", [], [""], [SECRET, ""]]) +def test_empty_secret_is_a_configuration_error(secret: Any, frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + with pytest.raises(ConfigurationError): + webhooks.verify(body, headers, secret) + + +@pytest.mark.parametrize( + ("secret", "message"), + [ + (None, "No webhook secret"), + (b"whsec_test_unit_secret_0001", "not bytes"), + ([SECRET, None], "must be a str"), + (5, "a str or a list"), + ], +) +def test_missing_or_wrongly_typed_secret_is_a_configuration_error( + secret: Any, message: str, frozen_time: list[float] +) -> None: + body, headers = _delivery(_payload()) + with pytest.raises(ConfigurationError, match=message): + webhooks.verify(body, headers, secret) + with pytest.raises(ConfigurationError, match=message): + webhooks.verify_parsed(json.loads(body), headers, secret) + + +def test_tolerance_boundaries(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + frozen_time[0] = NOW + 300 + assert webhooks.verify(body, headers, SECRET) + frozen_time[0] = NOW + 301 + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, headers, SECRET) + assert info.value.reason == "timestamp_out_of_tolerance" + assert webhooks.verify(body, headers, SECRET, tolerance=600) + frozen_time[0] = NOW - 301 + with pytest.raises(WebhookVerificationError): + webhooks.verify(body, headers, SECRET) + + +def test_negative_tolerance_is_a_configuration_error(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + with pytest.raises(ConfigurationError): + webhooks.verify(body, headers, SECRET, tolerance=-1) + # Checked before the body, so a bad body cannot hide the configuration error. + with pytest.raises(ConfigurationError): + webhooks.verify(b"x", headers, SECRET, tolerance=-1) + with pytest.raises(ConfigurationError): + webhooks.verify_parsed({}, headers, SECRET, tolerance=-1) + for bad in (float("nan"), True, "300"): + with pytest.raises(ConfigurationError): + webhooks.verify(body, headers, SECRET, tolerance=bad) # type: ignore[arg-type] + + +def _nested(depth: int) -> bytes: + return b'{"timestamp": 1760702400, "x": ' + b"[" * depth + b"]" * depth + b"}" + + +@pytest.mark.parametrize( + "body", + [ + b'{"timestamp": 1' + b"0" * 400 + b', "webhook_type": "status.updated"}', + b'{"timestamp": -1' + b"0" * 400 + b"}", + _nested(1_500), + _nested(30_000), + ], + ids=["huge-timestamp", "huge-negative-timestamp", "deep-1500", "deep-30000"], +) +def test_hostile_bodies_are_verification_errors(body: bytes, frozen_time: list[float]) -> None: + # Anyone can send these before any signature is checked; a receiver that + # catches WebhookVerificationError must never see anything else. + headers = {"X-Signature-V2": "0" * 64, "X-Signature": "0" * 64} + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, headers, SECRET) + assert info.value.reason in ( + "invalid_payload", + "timestamp_out_of_tolerance", + "invalid_signature", + ) + + +def test_deeply_nested_parsed_payload_is_a_verification_error(frozen_time: list[float]) -> None: + payload: dict[str, Any] = {"timestamp": NOW} + node: list[Any] = [] + payload["x"] = node + for _ in range(5_000): + child: list[Any] = [] + node.append(child) + node = child + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify_parsed(payload, {"X-Signature-V2": "0" * 64}, SECRET) + assert info.value.reason == "invalid_payload" + + +def test_str_body_with_a_lone_surrogate_is_invalid(frozen_time: list[float]) -> None: + body = '{"timestamp": 1760702400, "x": "' + chr(0xD800) + '"}' + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, {"X-Signature": "0" * 64}, SECRET) + assert info.value.reason == "invalid_payload" + + +def test_raw_asgi_header_pairs(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + scope_headers = [(k.lower().encode(), v.encode()) for k, v in headers.items()] + assert webhooks.verify(body, scope_headers, SECRET).body_authenticated # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("body", "reason"), + [ + (b"not json", "invalid_payload"), + (b"[1, 2]", "invalid_payload"), + (b'{"timestamp": NaN}', "invalid_payload"), + (b'{"timestamp": 1e400}', "invalid_payload"), + (b"\xff\xfe", "invalid_payload"), + (b'{"webhook_type": "status.updated"}', "missing_timestamp"), + (b'{"timestamp": "1760702400"}', "missing_timestamp"), + (b'{"timestamp": true}', "missing_timestamp"), + (b'{"timestamp": 1760702400.0}', "missing_timestamp"), + ], +) +def test_malformed_bodies(body: bytes, reason: str, frozen_time: list[float]) -> None: + headers = {"X-Signature": _sign(SECRET, body)} + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, headers, SECRET) + assert info.value.reason == reason + + +@pytest.mark.parametrize( + "value", ["soon", "", "1_760_702_400", "+1760702400", "1760702400.0", "\u0661760702400"] +) +def test_timestamp_header_must_be_plain_digits(value: str, frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + headers["X-Timestamp"] = value + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, headers, SECRET) + assert info.value.reason == "timestamp_mismatch" + + +def test_missing_timestamp_header_is_allowed(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + del headers["X-Timestamp"] + assert webhooks.verify(body, headers, SECRET).body_authenticated + + +@pytest.mark.parametrize( + "bad", + [ + "A" * 64, # uppercase hex + "g" * 64, + "0" * 63, + "0" * 65, + " " + "0" * 63, + ], +) +def test_only_well_formed_lowercase_hex_counts(bad: str, frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + good = headers["X-Signature"] + for header in ("X-Signature", "X-Signature-V2", "X-Signature-Simple"): + headers[header] = bad + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, headers, SECRET, allow_simple=True) + assert info.value.reason == "invalid_signature" + assert good.upper() != good + + +def test_uppercase_version_of_a_valid_signature_is_rejected(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload()) + only = {"X-Signature": headers["X-Signature"].upper()} + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify(body, only, SECRET) + assert info.value.reason == "invalid_signature" + + +def test_previous_headers_alone_are_checked(frozen_time: list[float]) -> None: + body, headers = _delivery(_payload(), secret=PREVIOUS) + previous = { + "X-Signature-Previous": headers["X-Signature"], + "X-Timestamp": headers["X-Timestamp"], + } + assert webhooks.verify(body, previous, PREVIOUS).verified_by == "signature" + v2_previous = {"X-Signature-V2-Previous": headers["X-Signature-V2"]} + assert webhooks.verify(body, v2_previous, PREVIOUS).verified_by == "signature_v2" + simple_previous = {"X-Signature-Simple-Previous": headers["X-Signature-Simple"]} + assert ( + webhooks.verify(body, simple_previous, PREVIOUS, allow_simple=True).verified_by + == "signature_simple" + ) + + +def test_simple_signature_formats_null_like_the_backend(frozen_time: list[float]) -> None: + # str(None) is "None" in the backend's canonical string. + payload = _payload(webhook_type="transaction.status.updated", session_id=None) + body, headers = _delivery(payload) + simple_only = {"X-Signature-Simple": headers["X-Signature-Simple"]} + event = webhooks.verify(body, simple_only, SECRET, allow_simple=True) + assert isinstance(event, TransactionStatusUpdatedEvent) + assert not event.body_authenticated + + +def test_verify_parsed_needs_v2(frozen_time: list[float]) -> None: + payload = _payload() + _, headers = _delivery(payload) + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify_parsed(payload, {"X-Signature": headers["X-Signature"]}, SECRET) + assert info.value.reason == "missing_signature" + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify_parsed({**payload, "status": "Declined"}, headers, SECRET) + assert info.value.reason == "invalid_signature" + with pytest.raises(WebhookVerificationError) as info: + webhooks.verify_parsed([payload], headers, SECRET) # type: ignore[arg-type] + assert info.value.reason == "invalid_payload" + + +def test_client_webhooks_verify_delegates(frozen_time: list[float]) -> None: + payload = _payload() + body, headers = _delivery(payload) + client = Didit(api_key=None) + assert client.webhooks.verify(body, headers, SECRET).body_authenticated + assert client.webhooks.verify_parsed(payload, headers, SECRET).verified_by == "signature_v2" + + +# Typed events -------------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("webhook_type", "cls", "extra"), + [ + ("status.updated", webhooks.SessionStatusUpdatedEvent, {"session_kind": "user"}), + ("data.updated", webhooks.SessionDataUpdatedEvent, {}), + ("user.status.updated", webhooks.UserStatusUpdatedEvent, {"vendor_user_id": "u-1"}), + ("user.data.updated", webhooks.UserDataUpdatedEvent, {}), + ( + "business.status.updated", + webhooks.BusinessStatusUpdatedEvent, + {"vendor_business_id": "b-1"}, + ), + ("business.data.updated", webhooks.BusinessDataUpdatedEvent, {}), + ("activity.created", webhooks.ActivityCreatedEvent, {}), + ( + "transaction.created", + webhooks.TransactionCreatedEvent, + {"amount": "10.00", "score": 12.5}, + ), + ("transaction.status.updated", webhooks.TransactionStatusUpdatedEvent, {"txn_id": "t-1"}), + ("travel_rule.status.updated", webhooks.TravelRuleStatusUpdatedEvent, {"rail": "TRP"}), + ( + "workflow.compliance_changed", + webhooks.WorkflowComplianceChangedEvent, + {"workflow_id": "w"}, + ), + ("kyb.registry_search.resolved", webhooks.KybRegistrySearchResolvedEvent, {}), + ], +) +def test_event_union( + webhook_type: str, cls: type[webhooks.WebhookEventBase], extra: dict[str, Any] +) -> None: + event = webhooks.parse_event(_payload(webhook_type=webhook_type, **extra)) + assert type(event) is cls + for key, value in extra.items(): + assert getattr(event, key) == value + + +def test_unknown_event_keeps_the_raw_payload() -> None: + payload = _payload(webhook_type="transaction_rule_suggestions.created", suggestions=[1, 2]) + event = webhooks.parse_event(payload) + assert isinstance(event, UnknownEvent) + assert event.webhook_type == "transaction_rule_suggestions.created" + assert event.payload == payload + assert event.event_id == payload["event_id"] + + +def test_registry_search_callback_uses_event_type() -> None: + event = webhooks.parse_event({"event_type": "kyb.registry_search.resolved", "timestamp": NOW}) + assert isinstance(event, webhooks.KybRegistrySearchResolvedEvent) + + +def test_events_keep_unknown_fields_and_survive_bad_shapes() -> None: + event = webhooks.parse_event( + _payload(new_field={"a": 1}, decision={"status": "Approved", "x": 1}) + ) + assert isinstance(event, SessionStatusUpdatedEvent) + assert event.model_extra == {"new_field": {"a": 1}} + assert event.decision is not None + # A field of the wrong type never raises; the value is kept as sent. + odd = webhooks.parse_event( + _payload(webhook_type="user.status.updated", status=["not", "a", "string"]) + ) + assert isinstance(odd, UserStatusUpdatedEvent) + loose: Any = odd + assert loose.status == ["not", "a", "string"] + + +def test_travel_rule_vector_parses_to_its_event(monkeypatch: pytest.MonkeyPatch) -> None: + vector = next(v for v in VECTORS if v["name"] == "travel_rule_status_updated") + monkeypatch.setattr("didit.webhooks._verify.time.time", lambda: float(vector["now"])) + event = webhooks.verify(vector["body"], vector["headers"], vector["secret"]) + assert isinstance(event, TravelRuleStatusUpdatedEvent) + assert event.travel_rule_status is not None + + +def test_public_names() -> None: + assert didit.webhooks.verify is webhooks.verify + assert didit.WebhookVerificationError is WebhookVerificationError diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a34b50d --- /dev/null +++ b/uv.lock @@ -0,0 +1,1121 @@ +version = 1 +revision = 5 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.13.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/d5/5dc1ee8a80c3b20a0b63c61b43ba6bc53517faecd1def47bcd752d22268e/ast_serialize-0.13.1.tar.gz", hash = "sha256:401cc84e8888d4d76f2c8d296fa006ed1a5ff993b6aff90dd777c03973efd025", size = 955137, upload-time = "2026-10-17T00:04:17.183Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/a8/fbd136b9f70c59e75f59a8e974c84102976f1201828b4f886480ba956936/ast_serialize-0.13.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3e129d44b0cffbea24a7f40117ce49e30993c3d119e584d4817ebd6f87246456", size = 897306, upload-time = "2026-10-17T00:02:52.479Z" }, + { url = "https://files.pythonhosted.org/packages/50/3f/2ad51e9823a790e7a70fd781e9595ab3abeace27f3aefcaa1939457efd72/ast_serialize-0.13.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:523d45bab16ddaf1399c04c4fe81bbf180f81e33c2f7bcbe76be56b0f7311ab4", size = 1236082, upload-time = "2026-10-17T00:02:54.82Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5c/8e959db5cc2237f9d4c560a8e2253c98f9746883fb4774651e4cda458901/ast_serialize-0.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:638333fb8ae72c62753f9e794fda20290883ddc65b0d85e26e08f8bb227567ad", size = 1221242, upload-time = "2026-10-17T00:02:56.395Z" }, + { url = "https://files.pythonhosted.org/packages/d6/05/ab1c56c3b7adcac88764bba609502d6b4989e449b2dd60eb478e1d20e369/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34c670747d85d4232834665e247009413eaebe47c69eaf60a5c439aaecaed9", size = 1282105, upload-time = "2026-10-17T00:02:58.275Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f4/41981fcd2ddffe0760e5326be9dcbd2eda9478a8430a637e5123bd8fcb26/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c8ebd419811c435812d242fb2a173e57dfede198c9ac9a0a584666ebc9b9331", size = 1287905, upload-time = "2026-10-17T00:03:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/6a/43/b783addc5681153707121991b7b47892f7e9c8ba58a0d03b71a3b023ba47/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8899c038b1c15ae8b98e379057fb3ef398ceb0b21d1737a8156dd5a0923b7e51", size = 1541806, upload-time = "2026-10-17T00:03:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/47/10/969db095038ee15270f65ad20d1129c5e4da605bf7e93b70ae640f1c8a9f/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e14c4de98f965647a74e6eaf37e20207124e45451ccd36e84a6f6ebab45e0372", size = 1309946, upload-time = "2026-10-17T00:03:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/06/dd/7c623d2fb0b868948952d1787e629a43b866f7d4caefd5445e20aefacec4/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3e8a3eff69af640db985360fef35073fb46956dfc3e5c614cf00920d2746dd", size = 1302773, upload-time = "2026-10-17T00:03:04.378Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c2/4d49469110a19a44fc6269f4fd59679bcf84df068f185f89b73bee4da19b/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:75d7750fe98f6f35a96c43aba8d98a2e623e22c778a0c17ab9ede92eb10f5d82", size = 1299579, upload-time = "2026-10-17T00:03:05.88Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/2d381f1f262dda15affc8ad643fba1619eb07e7d9f8bd904deffef183006/ast_serialize-0.13.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e8ddd63e80fc280c2f790157386f27f9b9f6ccb9c4f8e803b863cbfb085769", size = 1355969, upload-time = "2026-10-17T00:03:07.321Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9c/2266794e6c25fa9ce51ce04a9bf4b7e330e3ac9351ba3fa14e487678acdb/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9221e09496fb942901993c1812ee87950cc0ecbb514c7c0f1f5fa6d78f701ed8", size = 1460227, upload-time = "2026-10-17T00:03:08.733Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d6/14deb28e5d8f328f626a0778ea2cf98fe908f2bbcccd804cb6f449ee1782/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:7a5c38bdb93669c596f94fb7204dd6ebe4fadb19058ac9a2faf2948c2a175f96", size = 1562691, upload-time = "2026-10-17T00:03:10.25Z" }, + { url = "https://files.pythonhosted.org/packages/f5/19/7a28c0e5ba54f0d3e054f847a2a549b748cb179ab433321a8dc1b39e398d/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:acf4e9f055d75058ab01536da330648746f2f82b7697a85230547f2d5ebe895c", size = 1558669, upload-time = "2026-10-17T00:03:11.799Z" }, + { url = "https://files.pythonhosted.org/packages/18/9a/f9e2690eab58217f1d395a964a78efa7eaf813a1e38be8dfd9a8d45d0840/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87427913c325adc811e81e7c9a757b061f47ef1e29114ac479aa0953a2687a98", size = 1665071, upload-time = "2026-10-17T00:03:13.284Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f3/ef5b159a0d7161b404f05e7ec21cf7735ee7edc10c000cd2cbbc7bb6b69a/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ad88921f95b92bc144d9ad059a0e16f8c1ae69f5239f2d3c6536fd2774f73dfe", size = 1473364, upload-time = "2026-10-17T00:03:14.563Z" }, + { url = "https://files.pythonhosted.org/packages/1d/87/6e8ac4a865124a12cc4f915bf1bcfac3bba426cd5f58d3712a873ee61012/ast_serialize-0.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f8b17e2921e56621bcb46792bd3e0b82ca3b85de78d5936e7b8174ba1758b0aa", size = 1500566, upload-time = "2026-10-17T00:03:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1e/29155f1621433008013ad5c940a03d41a676be0680c41527040472d37583/ast_serialize-0.13.1-cp314-cp314t-win32.whl", hash = "sha256:2f9a9ce8b0994f757b941f7f7fe384a6666b4162b941ebc5c248081baea18e47", size = 1128674, upload-time = "2026-10-17T00:03:17.481Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/2506da65143ea138de5822e9173e0b72320a2f70e23bd723f044033175f1/ast_serialize-0.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5d99910e6b9a9b9360bcca2ae99aee36cc912ce058df2a57d1c33099b6cb2593", size = 1163306, upload-time = "2026-10-17T00:03:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8f/c7cb22638899b0cf15908809c270560e46395e65604bc59e4b623ab9f405/ast_serialize-0.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a789cbe14d0a0e9ec9b32a456e5abc81ad8799479c3cfd88a29e3442262aebe6", size = 1133775, upload-time = "2026-10-17T00:03:20.377Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e2/b0c140fe50fa39dd249e382f7733af6f816c90021fe7959a0b026f86237f/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:23d768369b89d551077dc173da6892fad00443338417523b6732f4e8ba4a7ed5", size = 1236160, upload-time = "2026-10-17T00:03:21.945Z" }, + { url = "https://files.pythonhosted.org/packages/23/c4/a8a2ca21e3943555b48221f8fba546877a7ad39efaf6505506524967d7c4/ast_serialize-0.13.1-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:68f8829d114dab6c9db4a8bbb8d0e342958faee1b5e15a29022678df639960f8", size = 1222214, upload-time = "2026-10-17T00:03:23.462Z" }, + { url = "https://files.pythonhosted.org/packages/15/19/2f58f2ba64e4799b39de85c140aef09a03aa6ea53be75dd292a9d9b9d8ae/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e385b17d5bde892a6184003a9aff3518387908b73f8f5ea539313c07c8c0ede", size = 1282979, upload-time = "2026-10-17T00:03:24.695Z" }, + { url = "https://files.pythonhosted.org/packages/b1/af/ca6087fe84210c1ccd43e3182917bb70649fa1e69745a90c1d59ba08fea8/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02686b281ec9b8e9614f6f0db27e12e4cac515f336c5d05b678bc9eb73ff2af5", size = 1288596, upload-time = "2026-10-17T00:03:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/b0/2c/4ef964c763329a0347125e745ec0451ba4fbaceec9efe2ac729b7cd55f17/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8ab2c2585b2e22e48cd4aff2136fce4eab4ced569b85f150ef9a5a5d6f34d49", size = 1545540, upload-time = "2026-10-17T00:03:27.52Z" }, + { url = "https://files.pythonhosted.org/packages/bb/85/aa961dbb75813cfa94de0ce9a16a9510c226b95f604c405421e727f44bb0/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c652d9f5d52d8b81eff3ff2c95c9dd0f281de45f6ad5d3fa95527550c396454a", size = 1309374, upload-time = "2026-10-17T00:03:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e6/f2b67c327d9f9533e41791804403ccc2dc457821195a2e51f0b587a90b90/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:121e1d08d58b75c15491cc3003609cbad71ac2b0016043c36d30a6b2032b6605", size = 1303466, upload-time = "2026-10-17T00:03:30.451Z" }, + { url = "https://files.pythonhosted.org/packages/73/1d/33f18b81d1bb1f5d682943ea92fda8130a6593627f12f6201668d40824fd/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:30cf89b6a6844542d116c758725d13de5b97b9410a51ee577715f761270d0c1c", size = 1300576, upload-time = "2026-10-17T00:03:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/25/cb/64914144925f1a4abdd9217e717b122255c38fc5ebe4342f92052f3bacbc/ast_serialize-0.13.1-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:732aaae9e498c973fc63fd7f308009b57c4afa6ff59d0b674d894e57d0bca01a", size = 1355960, upload-time = "2026-10-17T00:03:33.676Z" }, + { url = "https://files.pythonhosted.org/packages/ee/80/bd29379fab2bce19adc0bf9177002b11b23dfe8d8fecf409998b458b4624/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:d9c1a920b2aa0c1e016bed7721689717034ca0dba64ac3a3ff243b003d4e8dd8", size = 1459779, upload-time = "2026-10-17T00:03:35.09Z" }, + { url = "https://files.pythonhosted.org/packages/83/89/012dbe733de05ffdf8e40ca0f9fa5d417cb16ec93a74463c57c001914198/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:847cad4c22953823f5e76d0b126c6f9da670cfa45b1df41b9324e22189d77611", size = 1563399, upload-time = "2026-10-17T00:03:37.285Z" }, + { url = "https://files.pythonhosted.org/packages/69/37/ee4bcb0358bee38744eb0f18560446572d095fe3bf6b63d6f739d08a839c/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:aca07de3ca8e3d9f2b716c3c31f4f384fe4bd2d03e09912417cff7a3e08036f7", size = 1558808, upload-time = "2026-10-17T00:03:38.711Z" }, + { url = "https://files.pythonhosted.org/packages/54/1c/ff44c5355251d61ef22934c4430bb31a61de4781d25a992154aac9b8c82a/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:5db12d843d931b9d1f1519ce66b9cbfe72d8563b75c1204d255322254d1964dd", size = 1666324, upload-time = "2026-10-17T00:03:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bb/6da4bfc8aa3403e6447607131bf3463acaf954d990c6c8b6787c1902ddfe/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:fb8021f4dc3296de18737e3b8b34994411da725374d74a0b9eb32a738f2a23d0", size = 1474089, upload-time = "2026-10-17T00:03:41.715Z" }, + { url = "https://files.pythonhosted.org/packages/21/97/897e33bef2eefd880e6350e375cc66bc5bcd00a139be480fd655f5b3f095/ast_serialize-0.13.1-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:0d764913374ba21df8cd618045b371ed104ce55c6276de7c59e554a4f5ba26fd", size = 1501542, upload-time = "2026-10-17T00:03:43.08Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4c/821211ac83870c27e4412381b7c7838336456b40575bf840e4680b2018d9/ast_serialize-0.13.1-cp315-abi3.abi3t-win32.whl", hash = "sha256:2bad302ae2427454ef9dab71bd5c3ad67188860c39d36daee38f72031edb39c4", size = 1126118, upload-time = "2026-10-17T00:03:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/2b2d5e6c4fa352874a80618e742ffd32d249dee6c119f4233b51afc6b8f4/ast_serialize-0.13.1-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:032b09d86c785c3984e975378b7861f2a68ac44e22ba7b07b93a536b2524eeb8", size = 1163766, upload-time = "2026-10-17T00:03:45.986Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/d610cfcdabaa44fd62a54ca1fc57a7dba120643798b089a14914973562de/ast_serialize-0.13.1-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:929eda0c419e622ab49df3a4c45f81c57dcc10874bbad55d0470a4a03324dac3", size = 1134043, upload-time = "2026-10-17T00:03:48.453Z" }, + { url = "https://files.pythonhosted.org/packages/f0/89/0e30f9eddf7d44ccf9d25979030754fd0457af61fa4279acb8c293a04f97/ast_serialize-0.13.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bf53a3ba1cbe45399fc1e4ccd684b8f2a11cca450f89706b79122f478534c6f5", size = 897546, upload-time = "2026-10-17T00:03:50.093Z" }, + { url = "https://files.pythonhosted.org/packages/81/92/d705be88ff452b9ba0088b554a0cbd308fd01879da092b33fd2ad7d1aa60/ast_serialize-0.13.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4bcf1d5f4eefecaa325a34ecf079b2741189e7d0821523f92cb343a389a67489", size = 1242416, upload-time = "2026-10-17T00:03:51.521Z" }, + { url = "https://files.pythonhosted.org/packages/60/c2/98370845a7f35cb8a25782e78284b48e01bf3128339881d48d613f304f8f/ast_serialize-0.13.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1b158f1c9e3a0d386913924c57995d6df78224868a4d0f215bdd41587c97b34a", size = 1229729, upload-time = "2026-10-17T00:03:52.825Z" }, + { url = "https://files.pythonhosted.org/packages/d8/8b/2d7ffed76addd9d3fcf37fb171400942735c8912264c763561a85407f6a8/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a6201b69ce42e919f2fd9a559138d5410b5d8afff7c3a6b1f500b0481ff7d8", size = 1292255, upload-time = "2026-10-17T00:03:54.148Z" }, + { url = "https://files.pythonhosted.org/packages/6b/2a/bb9e0e719d89cc950fadfcfcc9cb4bc7640419c4228c852f031ab8abf440/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6eb0be64c9b8f1ed26637eef345abe75ea90351c92c85a8aec7838b3af971011", size = 1298501, upload-time = "2026-10-17T00:03:55.559Z" }, + { url = "https://files.pythonhosted.org/packages/7a/32/13ce07dce48226f5bd9cfd8f708ec101a8be05aa430cda4918ce31ce8c82/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f2427bddfca208e84de700044fadf9aef2769a70bae740a1bdabb89ee9e1117", size = 1553758, upload-time = "2026-10-17T00:03:56.952Z" }, + { url = "https://files.pythonhosted.org/packages/e2/fb/9b51d186db0c20ca28cbaaefd50f03b573c89327b9a2627820cbce22196b/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88df960e67184a71874e5b2ee014f87d1254a0dbc1f903c7bd7b1ab7733ecaef", size = 1318258, upload-time = "2026-10-17T00:03:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fa/da311559dd3dae9af5825cf21660775a929b970f72e3786af3f2cd667944/ast_serialize-0.13.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f99786e0d62082a5b121b3437f3167562133711df22128de1e9886f57f0b226", size = 1313260, upload-time = "2026-10-17T00:04:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/11/97a4b9e5b4776d72f47fcd9a09a497bbf68ec8c1ae074307a2c51e410d85/ast_serialize-0.13.1-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:a01b515b25a03184eff4acef25b981ae007916379682e908fc8785b342a0bf3e", size = 1309992, upload-time = "2026-10-17T00:04:01.575Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c4/62f85121a80cd451ef04f550cf0f7ea0ad9ff46cc4e9a5b9ae439b6527b1/ast_serialize-0.13.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:29422d7bcd9e7f39411571c36cec45ce8a7a9b3ad4b2e7aa64497c15a9e397f6", size = 1362715, upload-time = "2026-10-17T00:04:02.881Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/c5f4a545f72a7681e3295bab6e3600ee0862ec4391bed961fe9f735b67e0/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6ffb0f548a7dc126067fdfb7b2325ab5dfcdeecc29faf5e0f0461c96d6ebfa3d", size = 1469931, upload-time = "2026-10-17T00:04:04.181Z" }, + { url = "https://files.pythonhosted.org/packages/38/a9/65aed4f8fa7a001a7cfdc03e68e844517ab041ba6fb614b7d78a1d6ac056/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2876444a5f87a7c76f371af3c07c2fda563a08bc8af52568df812e8aba4bf568", size = 1570945, upload-time = "2026-10-17T00:04:05.778Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/44b2da991cede79bfbf52089f33a5f0168945466b3d9cd38b5dcf08e7130/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:885a62ec6481ce58ab24c6357360d7a82023b62dd7e5e7ceccf59e9662a3272b", size = 1569978, upload-time = "2026-10-17T00:04:07.347Z" }, + { url = "https://files.pythonhosted.org/packages/46/e4/109b2a5beca700237767d5add920c2f8e1ba9bd5799b0faf143d0e9cf4ef/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2015fd0165e463aac3b1aadc6a1fa1e5820c14ca950821f4fe02f8a3f6a388d6", size = 1676082, upload-time = "2026-10-17T00:04:08.7Z" }, + { url = "https://files.pythonhosted.org/packages/73/e0/7df70f6dbb11b5762da5416318f08316a1c51348e0d69ec79ca5f98f836a/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:eb41e1906502ac80d24ff343f45008552a3f5b5158607959fe6a07dbfc6eb037", size = 1483207, upload-time = "2026-10-17T00:04:10.162Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bc/523aa1c08bd69b5654621beb3dc1a9035715a4a63762fb8d219d6fb3eb81/ast_serialize-0.13.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ca9acc67f796aa2554fa3037dccc022a4b08b38b2ad153edfac9c9bb7cb1f3c6", size = 1512140, upload-time = "2026-10-17T00:04:11.589Z" }, + { url = "https://files.pythonhosted.org/packages/66/83/6607060659998f00afdea0d8375c1e6e2b7d515ab905090d9a5a02f48851/ast_serialize-0.13.1-cp39-abi3-win32.whl", hash = "sha256:4f98da562e5901d69c7dd30885594c5438f859edf354e9ac7517d188fb026c3a", size = 1131889, upload-time = "2026-10-17T00:04:12.953Z" }, + { url = "https://files.pythonhosted.org/packages/9f/6f/bafd6490fe7b355627fabe95f4417e3d02da93102c8552550eb9a0774e1e/ast_serialize-0.13.1-cp39-abi3-win_amd64.whl", hash = "sha256:ddf7cbc9c8abf70102c4a088efe154bb86bfb825b0a10e556d2b15972cf99bd7", size = 1171153, upload-time = "2026-10-17T00:04:14.362Z" }, + { url = "https://files.pythonhosted.org/packages/11/25/4235f4b375a67a37ac595bf478a5407a744eaf7b64113c1c3a5432b8d954/ast_serialize-0.13.1-cp39-abi3-win_arm64.whl", hash = "sha256:48f11d96c9ec71119612f8e79a8c55c12f2b136716d68347433c561d27a25f43", size = 1139035, upload-time = "2026-10-17T00:04:15.767Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/d2/2cde336b375f55c76ca670f0be3978cc048e31e24f3b4d7ce8473150a388/cffi-2.1.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be", size = 183779, upload-time = "2026-08-03T21:19:15.602Z" }, + { url = "https://files.pythonhosted.org/packages/94/1a/4b2f7c92293ba05cbd4a9a1b28faaf0326272d9488e6354657571c48a7aa/cffi-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b", size = 184178, upload-time = "2026-08-03T21:19:16.67Z" }, + { url = "https://files.pythonhosted.org/packages/17/0b/ba385d8ccedf926c3cd06e8e2f327027da5afe5f0eb30f1f7bc43ac55125/cffi-2.1.1-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004", size = 211037, upload-time = "2026-08-03T21:19:17.705Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b9/0f2e58b2cefa33255bff36935d42b13180fe559bba82596540eb404bde7d/cffi-2.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9", size = 218652, upload-time = "2026-08-03T21:19:18.735Z" }, + { url = "https://files.pythonhosted.org/packages/37/15/180e0dab27b9312c7479003d14c9e547634b7dcb934e2cc4650e1b131a7a/cffi-2.1.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98", size = 205422, upload-time = "2026-08-03T21:19:19.96Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/03026f0c850cbbaa9030750490225b4a7f4d524ea4df72c3cc740a90f4ef/cffi-2.1.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9", size = 205444, upload-time = "2026-08-03T21:19:21.246Z" }, + { url = "https://files.pythonhosted.org/packages/75/77/60bebf6f818bec84210ac5b6979ce4eeadce6fbbaabc9c7ab23e506d1ce5/cffi-2.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6", size = 218742, upload-time = "2026-08-03T21:19:22.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/679bf47e73fd77b352171727f07de559a003f14de5d02b904a6ec1fa73ca/cffi-2.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf", size = 221054, upload-time = "2026-08-03T21:19:23.694Z" }, + { url = "https://files.pythonhosted.org/packages/09/b8/eefc0e06913b70aa153bf74c946094a18f58fd4aff11b7f372bfdfdca050/cffi-2.1.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659", size = 213489, upload-time = "2026-08-03T21:19:24.922Z" }, + { url = "https://files.pythonhosted.org/packages/6f/13/4e56852824a03cdf68523a35686f1c28eacd4bd30a7b0a78e682e6e6e1d3/cffi-2.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9", size = 220241, upload-time = "2026-08-03T21:19:26.214Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/040f9e163e4acac3ee3d85b02d00b2576e7ca980d8785f0a3a5f1a9bf7f5/cffi-2.1.1-cp310-cp310-win32.whl", hash = "sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41", size = 174578, upload-time = "2026-08-03T21:19:27.338Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0b/644a2ec1a4eaba49c2939410bb1eb1d25b09d6d0582f5d2f95c537043725/cffi-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1", size = 185082, upload-time = "2026-08-03T21:19:28.409Z" }, + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/55/d1eaf3e73781174340a00dc1ba2aee8a65f82fadb18e2797b192b6b3925b/coverage-7.16.2.tar.gz", hash = "sha256:ca64d9f1f384f151b9511bec01126072acd2f313439f8ed015a22d8790aab6fa", size = 971999, upload-time = "2026-09-27T12:29:01.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/72/db17bf5f87568ab524413693385be2eeb03e652ab56c54ea05fa85515675/coverage-7.16.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23219888477edd736b6fcaec1272d47d93b926e999641ffea7e53a1738e70b2b", size = 223670, upload-time = "2026-09-27T12:25:34.675Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e1/285727a8a74d48de256e8605413ee4cf82301228253ea58e5ab4270138b0/coverage-7.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40c0f00899fe6181ae7f434ceb200e51f5ee4b8ed10e3b5f0b605f0cae15da87", size = 224191, upload-time = "2026-09-27T12:25:36.476Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/c737b73bac9bf5034f5ff45a4237b7a189d417a4e36751faf5d10c082cbe/coverage-7.16.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a4624f80732f6b427ac58f1f59c577a0994a12e8174b5af6a027b4b58795d4c3", size = 250921, upload-time = "2026-09-27T12:25:37.857Z" }, + { url = "https://files.pythonhosted.org/packages/29/4e/e1d38d27817d91776ca543d01276a67eace3df675a673cac32b12c167d58/coverage-7.16.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:191803c4996b499fcd78c2ad5e5f767dcc53cb4dc6de6d6a741b443a1821ef02", size = 252751, upload-time = "2026-09-27T12:25:39.291Z" }, + { url = "https://files.pythonhosted.org/packages/0e/19/14a8e44cbb2ad36ae62aaa03c5eec4a06a7f7df220ea3776af26927acbe2/coverage-7.16.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fd670ac43b709c575aefc25bf52d8a598a3bc5017bddfd0a179152ab06a2deb", size = 254616, upload-time = "2026-09-27T12:25:40.506Z" }, + { url = "https://files.pythonhosted.org/packages/f9/72/f5bcad0d9a9b450080032344fbff7ec60c1b0e3d38019f7735dee2b69645/coverage-7.16.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:705e5af11d34647efdc170c7840b6857c81cf74be96419a553f237e68e62cb72", size = 256541, upload-time = "2026-09-27T12:25:41.785Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9a/8c735234e8abb52bf5d063f98c780fa942e77c4c0225f9fcb9b33effc346/coverage-7.16.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8afd9bf35cc6a1f22eb3634808fa8e0b91902459c5721ef2e4461dfe771d7f08", size = 251589, upload-time = "2026-09-27T12:25:43.269Z" }, + { url = "https://files.pythonhosted.org/packages/35/6a/1bf6d32e55642d6972aa842640e0a8850e612d19ac7d05e2c32fa59dfd34/coverage-7.16.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f43bac1856ba269b905302778d4df433d6006489a192174ad77ac528e395032", size = 252660, upload-time = "2026-09-27T12:25:45.051Z" }, + { url = "https://files.pythonhosted.org/packages/53/5b/05b1c0d0e9495cb056155a16066259adc1e935e7411a82a62779bf729975/coverage-7.16.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f8475460aa33ee28ac896ab1156d0bb3b6c639f7f8383c2677d3359eb35f8205", size = 250660, upload-time = "2026-09-27T12:25:46.51Z" }, + { url = "https://files.pythonhosted.org/packages/05/33/5bc3db57fc9c56b4ef055725c38c34faa818a27d17d65e251371f7f0e3a0/coverage-7.16.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d6276d78f6fca7d0ac066d5da4165c5acd07829e8305c2cb900b738fb3a75a72", size = 254479, upload-time = "2026-09-27T12:25:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0f/4a5de66daef26eb919213f63b84f45d5065ab2df94cd767fc7c1174ac5e2/coverage-7.16.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:736fde09ea39646d11f8e3b76bd3425c075aa4dd45f24891970bb77c14ff20f5", size = 250940, upload-time = "2026-09-27T12:25:49.227Z" }, + { url = "https://files.pythonhosted.org/packages/57/d3/84cd6a11e707e422194739e9734d947746efa5e5b3358ff2d601f823ee45/coverage-7.16.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c85d54e7e8a2ca932fe8399301af9b8d5907ea2a455ffaff6e7d1208db83b943", size = 251543, upload-time = "2026-09-27T12:25:50.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/bc/3d84c2e2a95f38e346ce3730d9f0e53114f4e8aace72e6e51e014afb9992/coverage-7.16.2-cp310-cp310-win32.whl", hash = "sha256:5139009b5efd2194fc168ee9362f0e191ba612ef5d29242f9269c22f9b8f80c7", size = 225732, upload-time = "2026-09-27T12:25:51.945Z" }, + { url = "https://files.pythonhosted.org/packages/07/86/31f1f3170571a345ab9d8361a7b2f8e0c5698173fbecd1f7149b6e2089ee/coverage-7.16.2-cp310-cp310-win_amd64.whl", hash = "sha256:c3305c38a2fa21a4254f2ace7dd9ef5fc569c9a558b66e7017650b3d637fb95e", size = 226357, upload-time = "2026-09-27T12:25:53.272Z" }, + { url = "https://files.pythonhosted.org/packages/58/fa/ce3baf63d85b730398d92a7162f486f3a5e4e2cc3382a02488b3943725ba/coverage-7.16.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:732d950e51f3ba4fb6209c73250f3e8924fefca42953ee04a9e65d8c02414d7d", size = 223797, upload-time = "2026-09-27T12:25:54.756Z" }, + { url = "https://files.pythonhosted.org/packages/7a/57/9ba29c2aac7f756d479f03d45762120060f0f988788001001bf36e0e6fca/coverage-7.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dca0bb66b4c3d624ba047887bf70270030c150692d543cb501293dc38a9f4b5", size = 224303, upload-time = "2026-09-27T12:25:56.214Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7b/0d6d60906dca7d28cc1e3fce12a9861801c4fbb6cbf220ad78cd059c9467/coverage-7.16.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:af2a2a8c7c74de0559e0c368d94c8def9e16c58faaee33a0bf081057c4227e3b", size = 254715, upload-time = "2026-09-27T12:25:57.755Z" }, + { url = "https://files.pythonhosted.org/packages/cd/b8/9198b865679379fb165c689c64f6e11105ef380f6bd1c7673e83f73d9f5c/coverage-7.16.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db5f8394e17f877a625b257f2ba0ce8e728a499c2c1579ad66220272cd3df510", size = 256627, upload-time = "2026-09-27T12:25:59.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/79/9521462cb6072fe394701bc8974b74afd576c9c9355156c7844e1a86a42b/coverage-7.16.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b3146d2317c75f70df2509066d979dadd941f7021cdf9b5db4bcd8568258e25", size = 258724, upload-time = "2026-09-27T12:26:00.691Z" }, + { url = "https://files.pythonhosted.org/packages/a6/76/8d7d5d633db9fe0f3182fedc731bf09f9bcf2366055735152504ad614677/coverage-7.16.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e1d0ced76318bab499693ff25f64faa343415187cb2e4d7befdfdd391a1cf6a", size = 260692, upload-time = "2026-09-27T12:26:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a7/76cb09c89ba46d74d37428bf93251fc14fb0bbe9e05cc2a5ef61773d318a/coverage-7.16.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:af98ad5ed9d6daaca956201e00bb429a7eb2b080426686f70a20353e0f9839f5", size = 254800, upload-time = "2026-09-27T12:26:03.369Z" }, + { url = "https://files.pythonhosted.org/packages/72/b6/2351c1979aaeb5b4a8091a75b90ca997ad60de36e181ddba267cf61dac97/coverage-7.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d56e4d21c56d2046447733f8b118409597db48c01efe898ee9ac24e858ec2d6", size = 256423, upload-time = "2026-09-27T12:26:04.751Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f4/ad9a4f8b5cb2d494fa9452b546fe742ed2f9d3847cc14c05e36279a3e649/coverage-7.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1d5d0e3b660506fb84f995814e3118a21efdc0c8eb80127da1be627d90093c17", size = 254487, upload-time = "2026-09-27T12:26:06.082Z" }, + { url = "https://files.pythonhosted.org/packages/6c/1f/a520470472f3e8b01169bf42162b1470c9ba992230432f62ca36269bf3a0/coverage-7.16.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:17228fbca0f22976f797be94e975dcd237799c657d49551c7de1e0654d1202e9", size = 258259, upload-time = "2026-09-27T12:26:07.513Z" }, + { url = "https://files.pythonhosted.org/packages/09/d2/ff26d5938274745855fa61cfcba0245c88ccc10d98d2cbd96064f16cd5a7/coverage-7.16.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:bc0b0ac781d489304b741269857f1f8338b7a26b1b89c06c0344658001ec0035", size = 254216, upload-time = "2026-09-27T12:26:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1d/5d832d3b06785d9f53267e4f2724a9f60c312eee6ebed9063a461d0d3b45/coverage-7.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bf1bd822ec4e387ed245bed0d71151582cf7be9e5309bc4145eefe36083d5878", size = 255125, upload-time = "2026-09-27T12:26:10.35Z" }, + { url = "https://files.pythonhosted.org/packages/55/4d/1d33edbc2fcf7d99e384e393e712aa5a2ebbbd8409825357815982207976/coverage-7.16.2-cp311-cp311-win32.whl", hash = "sha256:7ed238d227e23cc300c3d464babdaf9f6ddc740aa1b15a77ae96136e6a7c4516", size = 225898, upload-time = "2026-09-27T12:26:11.7Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7c/676df4882118756c4f8f560c954eddb93e166d84dda8c5f0b6a829689bde/coverage-7.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:a90700f743e29aa3d75a6ff5f01953176a889c00e526194bc4d281731b88d99d", size = 226369, upload-time = "2026-09-27T12:26:13.375Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0e/a457f4a461b3c5610d845137fdd45fa465e011a64c25af440518ab1f4e41/coverage-7.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:a336eec40e3520d369b8a6cdabb4f596e69a8b42927ca074aa1452fed943238a", size = 226344, upload-time = "2026-09-27T12:26:15.127Z" }, + { url = "https://files.pythonhosted.org/packages/5e/2c/f8296c63c5d542f3d21aed685e56b7031a419037d155bb3382fc0940d249/coverage-7.16.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:218d742afca2b5ad5ca759e93eddedfbcc6eadf8322f080dcefc40b7bd4e2d48", size = 223969, upload-time = "2026-09-27T12:26:16.753Z" }, + { url = "https://files.pythonhosted.org/packages/90/23/6f3dcb1423a0d43216e402ea1746e4a7c7c44f38896b97dd573790f56a40/coverage-7.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9a638be322a8d76a41cdb17781c7f82aaee6a66493d8ffb7e2c09ee22423d99", size = 224328, upload-time = "2026-09-27T12:26:18.15Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7d/8f3b6dc920e3fc6732f7678785a2091db439f186afbec30dbf2214d9b1f7/coverage-7.16.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:724bd0f1e81856b35e59fc98cf7b4e544a3cb662e4e0864dca73d4326ee9d808", size = 255832, upload-time = "2026-09-27T12:26:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/6c45f15be4eca4ac1062c6a55a323286494c99726a7e58951fe85967ac08/coverage-7.16.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5375ebd99038021b35e99dc88255022912c06565d316212f4a576e4b08d30f5d", size = 258571, upload-time = "2026-09-27T12:26:21.199Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/b54cbeba3ad89082c2e441278681859e538322cc34b84b2af7ebff00080f/coverage-7.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a076277ca9f5750cc230f0f578ebd2620cec60255b25707361699fef6fb465c", size = 259680, upload-time = "2026-09-27T12:26:22.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a2/0dc65ec3d61930e1e4c2e371763b15eb4290896eb343a12d5d3091308116/coverage-7.16.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:58d4a54c6ea672afef66d49be922a2c69826c5ae1a42a9cd94f0c9c2bacdf800", size = 261945, upload-time = "2026-09-27T12:26:24.336Z" }, + { url = "https://files.pythonhosted.org/packages/d6/93/5fad7a61f2c14e08e98946fc31c1c7ffc1195061bf3fdc351db3be77a863/coverage-7.16.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcbcfcc059117284c603ff8cb61a65872512882f84a8cf0339241f7f7c2f148", size = 256191, upload-time = "2026-09-27T12:26:25.89Z" }, + { url = "https://files.pythonhosted.org/packages/2d/47/74e5de9227b939ece9f64e729645ddc4296bea10dbfa98721c1333c8be2e/coverage-7.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afdf43b72ef3876c1fe66423b91466e37877c9e81e8cec70542b7e8525b9d1b7", size = 257610, upload-time = "2026-09-27T12:26:27.35Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/2cf28d40b43645d1b72388fe3ee7f7c747533a6a9557bb8c24a7ae74fe1a/coverage-7.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9acc7f7ec4a1b5f89bd929fde5b8a714f6fafdc6cc18725413d510aa082b47ad", size = 255757, upload-time = "2026-09-27T12:26:28.949Z" }, + { url = "https://files.pythonhosted.org/packages/d7/3d/7c149fd99fc8bbc39c80db5e688d1d39fd040be2ecb78b8335a51a55b9c0/coverage-7.16.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:80d3f7b48d43ee8fc5e8707a8adb43d743a5a1a85256c25a24f9d6d0e2238fa6", size = 259822, upload-time = "2026-09-27T12:26:30.515Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/b283fce09d5995e227bd8e513358dd7471bedc0f78abc85a925ebdb0a2f6/coverage-7.16.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:126d1af8804d7224421fe991ff65d3ce649081560df7a98b1a5ffff07f9923bd", size = 255325, upload-time = "2026-09-27T12:26:32.037Z" }, + { url = "https://files.pythonhosted.org/packages/bf/91/f3325edf0c4223fb1fe1532b8dbef2a1d2f729459a9a7d1a44d073bae534/coverage-7.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c19cd6d025c1673f22afcd22c7df8a662d779e05d8e3fa6820c22afb895b0206", size = 257191, upload-time = "2026-09-27T12:26:33.525Z" }, + { url = "https://files.pythonhosted.org/packages/c4/89/21eb5e83ecf2eed523c4eb3d65ae513cd082c8fd1b6deb34c4cb6c332f97/coverage-7.16.2-cp312-cp312-win32.whl", hash = "sha256:152877cdc8a07264882cfcd503ba56a3ef6cba56a70e8c70f6eb8ffd7384789a", size = 226034, upload-time = "2026-09-27T12:26:35.021Z" }, + { url = "https://files.pythonhosted.org/packages/db/de/e3ad6d864c0833624b4f1f9b53f9e58e116c945e5e965c3f1e172c5e84cd/coverage-7.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:e6c52d3307824ff93b39efd99e4185d557db40bd841452abfb32e5d9151ca162", size = 226569, upload-time = "2026-09-27T12:26:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c1/bccc58ebe5489cc70628f635c1932fd371f5d7da850dbcf960f95f4c4afc/coverage-7.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:a678c0b6b22086ec2427359d22e37445d4a792f5fdbbc744112c7dade65cad02", size = 226372, upload-time = "2026-09-27T12:26:38.406Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f6/8eb4f220ef24f84fb27d852d4f9bf83e0c73ec1a4a08dd9a87e3f4529739/coverage-7.16.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1a37c6e478cf687e1aa30a593d19c92c02fad9d122b51ab73f51b8dc7a0c0fc9", size = 223993, upload-time = "2026-09-27T12:26:40.164Z" }, + { url = "https://files.pythonhosted.org/packages/40/23/d4bbaf0c154e0b0c2b5264890dbf6ef098dcb50ec8f2469be9490d191660/coverage-7.16.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0993d0e90858c03943d3cb152e068a20dd4707924deec84dd2230261baae3b1b", size = 224368, upload-time = "2026-09-27T12:26:41.762Z" }, + { url = "https://files.pythonhosted.org/packages/7f/48/fc1e88fd571ec5cb38150b7f89f7696ca1bdf9920e01432febb69774cc85/coverage-7.16.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bb2fc905bbf4e6b7f40806ea79e31515abf6349594cdf0adf27c4215f0463204", size = 255358, upload-time = "2026-09-27T12:26:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/1d/56/6785397d07c29c8e70fbb9a07e97d062b43c21ffc5f12385917847f09f63/coverage-7.16.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4358b9c8c0125b460407f3017c6cce8156e904b32772c5630d27112f52bdbfe5", size = 257955, upload-time = "2026-09-27T12:26:45.725Z" }, + { url = "https://files.pythonhosted.org/packages/27/3b/c8cdd07721e5f99abd81cea970d971997f99bf158c0b85f51bd284179c8b/coverage-7.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f15254427c9b33eedac4f198eaf9e356eb4f6214551afb43da6194a2c088ad7", size = 259183, upload-time = "2026-09-27T12:26:47.208Z" }, + { url = "https://files.pythonhosted.org/packages/9b/11/606b192fe43d32574ec6238549d48de588fdcc18485682a5ec0a8ac357f2/coverage-7.16.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9a75a4704ff640e46170042eec1f984385a121227c505d5a16ad8e495f452541", size = 261325, upload-time = "2026-09-27T12:26:49.084Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/eea481f8b0305ceeb33f081a5f47e298391dbd1b589de0c4b3b3aa50d3f2/coverage-7.16.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:14253fc7bb15749b849795a06f5d3b6d8bc3fb8a4b5ddc341faf7a89dce205fc", size = 255531, upload-time = "2026-09-27T12:26:50.509Z" }, + { url = "https://files.pythonhosted.org/packages/6b/be/dedbf9aea1457b120c27ac10b8fc2a357f37fa2b54c3e7286d42980a0a2a/coverage-7.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:921415102a90637fcc2e3f169f61dad7699ecf690e8639fc21b813acbedc0967", size = 257329, upload-time = "2026-09-27T12:26:52.005Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cb/b25c19d5bb2bd0f2e4e27fe8e2ffcae80c7a91ae181c0dc749ed60e9b1a4/coverage-7.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cce2bc991293f15cc4084ca116827b5900c5f34e1a54dfe83f10ab5c43162eb7", size = 255294, upload-time = "2026-09-27T12:26:53.634Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a2/892c5c5f4ad44b7b2ca009aee705191f3f268f15052244f2f9e3539b2e35/coverage-7.16.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1fa594c887365b69745f25a416806e61085dd07b94c9eae68a6e20730629b23", size = 259444, upload-time = "2026-09-27T12:26:55.243Z" }, + { url = "https://files.pythonhosted.org/packages/ed/99/a562537deba0a3e370182ae71c149be796c39d8087365f17a09188f27145/coverage-7.16.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:11e597173af1dc33d5f8a7332ada544199269a223af1ee1770ddd5e245ad0fe8", size = 255111, upload-time = "2026-09-27T12:26:56.851Z" }, + { url = "https://files.pythonhosted.org/packages/2d/20/854ec68641a9b3362ff068a32dfa41637299761617ef253791dbade6fc76/coverage-7.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3e7f99698ba3a7d13988bdd984b7ebf13af4dbe2166dc8502eef90d77603b0a4", size = 256882, upload-time = "2026-09-27T12:26:58.41Z" }, + { url = "https://files.pythonhosted.org/packages/db/0d/748e4518b0ac0f9ff2687c248a6e5f8c0737306e709372632a2556f84443/coverage-7.16.2-cp313-cp313-win32.whl", hash = "sha256:f80bd9f9633eafc73d0a913ba2645c96ba58bba1befc30590f7c0fbfde59d865", size = 226045, upload-time = "2026-09-27T12:26:59.983Z" }, + { url = "https://files.pythonhosted.org/packages/31/fa/6e46edba66a183fe4d99d4bb52c173287e9b8dddabe0888d24cb8210e580/coverage-7.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:8be099e979fc42559328a21828281b4578304191ae46ed4e80a407048a82eee6", size = 226581, upload-time = "2026-09-27T12:27:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d9/9ef6845367600b336ff75d000444a0d32497d6972c833141bd39356abf68/coverage-7.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:28ff850182a67d117990fa2ce5ea1032836d8c9630dae867e8bdd3bff4533b79", size = 226409, upload-time = "2026-09-27T12:27:03.116Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/577fc0803dab4155dcf808faffbdd7b159256781c0874a8586e17b81b149/coverage-7.16.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4ee546b9e4872ffa194bf07ac87bfa1202ebb824d0795dc1ef22f175545ca90a", size = 224138, upload-time = "2026-09-27T12:27:05.141Z" }, + { url = "https://files.pythonhosted.org/packages/75/9e/e3785ba3ecba2bd11efc74bfe2801ca4b78c4480b15a375648d809a59da3/coverage-7.16.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a2fac6895eb299a2e52d7bbb8fb3903502b9da8d3f5309ceb16ec40c646b58ee", size = 224445, upload-time = "2026-09-27T12:27:06.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d0/963ff22d3fd27117da3b8cc442f5bdc91196f783321e1a8ff0ec43476772/coverage-7.16.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:57ff3783f99d75a1e81dd56a9737eb5665e6736a5d93258ba596b6dcad8fd05b", size = 256112, upload-time = "2026-09-27T12:27:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d4/a306940c81c6ae759e82fff27d20b7fdc6896e422b821f51313cce212b6c/coverage-7.16.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35f37886699cb9abd29958247d718628d5bc6f39e623dff66a09e546c42a7e03", size = 258727, upload-time = "2026-09-27T12:27:09.927Z" }, + { url = "https://files.pythonhosted.org/packages/b9/a3/d3d99d93b02517087aa05bc0cf2d04d372956b849e5443e059079901429b/coverage-7.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0fd7a86fdda7cb6d616d178654bd0ad6bc0f3f33c2e478aa598500a1a9e34eda", size = 259909, upload-time = "2026-09-27T12:27:11.55Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/39dd599181726758dd185ae4dc0c0ab3aeabf7ca70e68e145060feeaaa16/coverage-7.16.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac0f3b379c94acc2f7dce5f5f0b24d44fa1cc6a509717ef83dfee07450c2117c", size = 262481, upload-time = "2026-09-27T12:27:13.17Z" }, + { url = "https://files.pythonhosted.org/packages/99/e8/91ee43f6ded411460c359d7e1aebde4d6fd8f00a2e5394182d9d212eb23c/coverage-7.16.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d0732c83746bc24123c581a85d9dd96b70ddb538c9076020aa1a041790361e9", size = 256012, upload-time = "2026-09-27T12:27:14.91Z" }, + { url = "https://files.pythonhosted.org/packages/11/8c/e9499ddc33197bd7eabcb1118ca81756fc874457b324e2b479a4804b2ad2/coverage-7.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b451c68218c150f616bc9649783ec8de76a59792c759b43aa0c9c0466a465e4", size = 257898, upload-time = "2026-09-27T12:27:16.588Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6e/c081cb5991a0afba99f9c4ad6c74a5fce9513a38ddc64e3e6680c6fed9af/coverage-7.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a56ac4fa5a75c7e182e8f62600cfb4aff43c5ed7356a034f3557659c3bec1d90", size = 255938, upload-time = "2026-09-27T12:27:18.19Z" }, + { url = "https://files.pythonhosted.org/packages/b2/42/1c3d819e8f9b6eb01c2fe90874d67a8882adb9507e0bbb09361ed131ea89/coverage-7.16.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc4f73aa3fabc36e32046d6cd2971405948d8a903636508a3d3b2f9128b3a95", size = 260368, upload-time = "2026-09-27T12:27:19.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/4f/d70eac07901fd587b6ab05e659b52afe13959992aa5113bf6cce059cc572/coverage-7.16.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:723dcdab91357159b722935b500ee8abc0a66c8c432e1e9fabf4cc7598952de8", size = 255620, upload-time = "2026-09-27T12:27:21.621Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/6d87af88317d3d9a9b18a9ca1bc1673eb516917f296e579d0d4a55cb3490/coverage-7.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5397e21a90dde0e9c6896b77ded8f0be26b66f8b22b33aed41f6043ed95d55e6", size = 257521, upload-time = "2026-09-27T12:27:23.358Z" }, + { url = "https://files.pythonhosted.org/packages/79/bb/90c2641170d2fa1a6757b3f8450ba2740197317b0ddd749e9604b914e886/coverage-7.16.2-cp314-cp314-win32.whl", hash = "sha256:848893e1d361448c113dc2f0913503522a6f7be231d0e38333d2a22d9698a011", size = 226248, upload-time = "2026-09-27T12:27:25.153Z" }, + { url = "https://files.pythonhosted.org/packages/30/08/d8d0478bb02c8eb0ae20a496fc80c40fcf4d3450bd184300d682ba2d28a6/coverage-7.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a27b731c171e43dc8b5f32b76a5051dde2ec9b9366c87028f08a7088ebc2c7b", size = 226732, upload-time = "2026-09-27T12:27:26.907Z" }, + { url = "https://files.pythonhosted.org/packages/32/3f/0001da22155b0a8ce063ec0f7e64ecbe17b373f306e7a74435f6d6accb72/coverage-7.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c569a9fd25505f1cd6bea90588818f90373ce90e2632e2cacf19ddbd6e14fdb", size = 226645, upload-time = "2026-09-27T12:27:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/6d8813aff9b8b8586691a9d33c43c5604f7227622574da7cdc3d91a86861/coverage-7.16.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d93db87adb6b1c1b408dce4763314b55d76a9f589e96783a84ac9e7689e48bdf", size = 224871, upload-time = "2026-09-27T12:27:30.32Z" }, + { url = "https://files.pythonhosted.org/packages/5c/70/444f3a4981ac2cda40fdcf4cc9b56a4e1a33c222abeb33e51ed3e3eb2a6b/coverage-7.16.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aa62c85046473959c13ba9edca9dc90a77d5c1095b1ba313556314d77fe5b036", size = 225123, upload-time = "2026-09-27T12:27:32.33Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c1/980681cd7b33eb66ac835044116ef0a92e11fcc7bdd866cc89d10b1130b9/coverage-7.16.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:db76506aa5416081f3e8974ae0f7965c58ada0bb0ef7339ac86099588dbb20d3", size = 265049, upload-time = "2026-09-27T12:27:34.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/87679875c33bb2191f0f05544a1cc9adcc940fe0c35443a10f2df753dde5/coverage-7.16.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a0f2285329dac10ab08f79cb11f5692c497018e6c7c511f95e6fd63a70b8f831", size = 267717, upload-time = "2026-09-27T12:27:36.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/64/5d372776d6eb523d4e93bafba2253f96984e3b18261c4cc56a50863c6d0d/coverage-7.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:382d3346d56b0eec1b793d53a4c88799c8053f516aa3a8d7c44315696954bacf", size = 270047, upload-time = "2026-09-27T12:27:37.96Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/44082ff0cbf9f97d0043f57970a71204097ec7ba606361a9fd2065393669/coverage-7.16.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:648352b94507179d82637292e7ae8802508d95f78e2f00a705a50b6c48011681", size = 271360, upload-time = "2026-09-27T12:27:39.766Z" }, + { url = "https://files.pythonhosted.org/packages/b8/17/9a215efe25b5e0ecc87c89dbe525c4a87d14d87c8c0c7316ef140a5f6f3e/coverage-7.16.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fb2bde05838fffae1a1bf75e5d411a6cac3e4e9bb97e6640fed8cd47888b33f0", size = 264736, upload-time = "2026-09-27T12:27:42.072Z" }, + { url = "https://files.pythonhosted.org/packages/a2/da/7f0a31af8e448107d4d32844bd684757f51ea907bc0c68c8fd537b2123ff/coverage-7.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6a75180829efb8ae62b4aded25be6ddca1c888d138d2d82e21d93bfbd88f41cb", size = 267600, upload-time = "2026-09-27T12:27:43.85Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/3bfecbd3366b775bacdcb3330394d356cf384b5d8f5b2146ac4b14b252b5/coverage-7.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:99704f73721e23859112072d522076e11c31744fc96b5652e5dd2018aa4359f7", size = 264767, upload-time = "2026-09-27T12:27:45.768Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3f/5d62163732d87e4a0c4710a0eab30f0fd6a2d480112abe2029f014fe8c9d/coverage-7.16.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:29309ccc86b7f33df7db12813c299f215bbbc470ed6292d0bedd63ffae1ebf64", size = 269026, upload-time = "2026-09-27T12:27:47.787Z" }, + { url = "https://files.pythonhosted.org/packages/49/4d/8e4579f225426535085a9be371cc75e3b026d058d679b80affbdfb4c3ef0/coverage-7.16.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:30c1b65d529e46569899fadca59e4a87c1faf2886923f1307ba61e654d4f3c20", size = 264147, upload-time = "2026-09-27T12:27:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/d1/36/ef1f77e2c3f7bb03c2b13b9a2006f88700fdd75535ef158d70049f425c1c/coverage-7.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcf4bc2aab4e16b1c4c0c2005918f23a7dd5d7821ddae82caed9e3342dc2fcce", size = 266376, upload-time = "2026-09-27T12:27:51.551Z" }, + { url = "https://files.pythonhosted.org/packages/be/79/0cb2bf4428830dec971c718c2c841a039c084415c99e67281f5a72841aab/coverage-7.16.2-cp314-cp314t-win32.whl", hash = "sha256:a9cd3de0a5bfe7b0e21ee10e1a14e3d61bf52efc88217ab1d95d6ace6970bd46", size = 226585, upload-time = "2026-09-27T12:27:53.945Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f9/da17121c16667fd84998e972200ae226a41540f6ea4795776c6d99e8976f/coverage-7.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:611a44e5229a59d7483ce830160e1a0e85f700562c7a5651c7c63fb8f4eb528c", size = 227378, upload-time = "2026-09-27T12:27:55.778Z" }, + { url = "https://files.pythonhosted.org/packages/74/89/01179c62d1b7e6e33bd5001566b02d7f778cf33d3ec1e81e94ca170c517f/coverage-7.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:22957cef43ce038641de78ba995de7568d2d6a37c6ddbf7fa0fd7d1ae2344d91", size = 227064, upload-time = "2026-09-27T12:27:57.496Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/52935003c3f627ba6e5203d7179aad32448c10899663a30336aba8e81a2c/coverage-7.16.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:414c26dfdb96aac2d570a54e03008f001e32eb2d413705365503648c6bd361d8", size = 224140, upload-time = "2026-09-27T12:27:59.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/df472520f3e626524d7e2fc9d6da0afe7895a2f1489d36b48af8ca40bb41/coverage-7.16.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:00d3eb96e9988c45f50cccd1f1496571ac5c1f91386ac02c4d55516eeda19a24", size = 224449, upload-time = "2026-09-27T12:28:01.299Z" }, + { url = "https://files.pythonhosted.org/packages/0c/aa/3be084d5b82e63ccdad4ed751e4acbae294673573e30481d29f8b7402eec/coverage-7.16.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dbbd1155ca46e6e0b6b89d204428c56ef6a459af21333f365d135a2820e5a09", size = 256060, upload-time = "2026-09-27T12:28:03.185Z" }, + { url = "https://files.pythonhosted.org/packages/de/29/48fca82a7ebf7ff7b2e35019cc9537e7f65e4d2aa1215cc5a8792c989251/coverage-7.16.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8fc15cc8d0d06e873c00ef18e1372d605f9aaf3de27d8c24e50782e75bc8b843", size = 259106, upload-time = "2026-09-27T12:28:05.15Z" }, + { url = "https://files.pythonhosted.org/packages/06/3d/b2d5986f2dd53fe201aa1be2e4ab204fa1aed5101e67c0dbbb419b850aee/coverage-7.16.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c6afdd69218202bc1758c9a14b86b8cf1084f37ed2ca143e567a103772b16d1", size = 260674, upload-time = "2026-09-27T12:28:06.868Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7e/b50160be3506ead12e6480d14279af7f0f17627694300a2d1fd2c42d2ff5/coverage-7.16.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:aba5c63b7afdc749cc9eae943d5b868cba2b261a176378fa1c5a30bc8bc89982", size = 263139, upload-time = "2026-09-27T12:28:08.771Z" }, + { url = "https://files.pythonhosted.org/packages/14/5e/7c805ac9a32606de1399bd7e9bd375aa2f973dc61b12680d9e6403c2e891/coverage-7.16.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9174f0af24e5eff248b9dbfe76ec5275a3d19d37edbc2810543f12cf97347a34", size = 256591, upload-time = "2026-09-27T12:28:10.842Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9e/76f1ed129a2daf658a3ea17122824cf2e3b91fea0460d8d3664fc5a61018/coverage-7.16.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:80e9fdb4c3d926b6ba721d4bf7435bdb869c3527ae7803290361d0ab73db13b6", size = 258708, upload-time = "2026-09-27T12:28:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b7/8d62e75f48b527619239a65294f842d4b7fd02a0839d43ae1de80184e2df/coverage-7.16.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:7b3bce4a0d05401d70b7d0d5ca783e686bc9d30e81dbd7d980d532609bf809e4", size = 256562, upload-time = "2026-09-27T12:28:14.934Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/0a15f95c3afb78e947c52644786ba4bc9de259905687dd720d5e6fae2e76/coverage-7.16.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:44f21e407b278efdfc1ee5e481e00518bd1d500310a30a5fbf2bcbedfef4aaf0", size = 261077, upload-time = "2026-09-27T12:28:17.215Z" }, + { url = "https://files.pythonhosted.org/packages/25/00/88389987305a47d732866c07c8a500000ab574df9505e3114ac69c8d027f/coverage-7.16.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:59c3926585e1cd1f2190f4b2ac9014de1bbeaf0d5d0587b0dc6b0aa90d17896a", size = 256034, upload-time = "2026-09-27T12:28:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/92/02/34d079d4952ad461bde037d353f9a6e037a7edc45fe0f9ee8781ff73f028/coverage-7.16.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:066429634299e14dd2d511e1e85f8f9cecc500781f6b41907c0dd6f1baea7e63", size = 258034, upload-time = "2026-09-27T12:28:21.242Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d8/3e59a62879285b464ec1b10fd824fbc1af9ce66e842cd39974f80a0becc4/coverage-7.16.2-cp315-cp315-win32.whl", hash = "sha256:893ea9cf86cb8d2546812ac93d973aaf2ee1fb45110a873b014214fd23e3725e", size = 226252, upload-time = "2026-09-27T12:28:23.102Z" }, + { url = "https://files.pythonhosted.org/packages/f4/e1/128026e1b2836e9ad6b219207ba9edf1c5e0088a7869e23088aee7fbbe7a/coverage-7.16.2-cp315-cp315-win_amd64.whl", hash = "sha256:01c6908bc613b420c26c818fe948e1b97dfd041a53c98b01c63bd8321f5c9aae", size = 226728, upload-time = "2026-09-27T12:28:25.21Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f4/c9fa8e7cf525ca7748ac52b0ee89331d13fe09808e45c679830708782e90/coverage-7.16.2-cp315-cp315-win_arm64.whl", hash = "sha256:967d72c835d7a8cf0af99ec813a2d06e3db6df706402f1fe85b31b437645f495", size = 226648, upload-time = "2026-09-27T12:28:27.136Z" }, + { url = "https://files.pythonhosted.org/packages/a2/13/e96b045447a856666f36f9c653e2a80bdaa732aaaf72412b19aa2c26a473/coverage-7.16.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:98d9c97f51b334b0adce7b964442a9af33c1a00c6ac856984cc5dc8d18f81c75", size = 224858, upload-time = "2026-09-27T12:28:29.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/90/087f6ad1bd3df059632ca3407a4e6552ed1053ee35354de0a771acf35423/coverage-7.16.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:3e861f1071dcc2fec1e88bef0920f6b1eaa66a143555b4f8ab79ba2b0f30ef55", size = 225139, upload-time = "2026-09-27T12:28:31.131Z" }, + { url = "https://files.pythonhosted.org/packages/7e/8e/285dcef0184358044e7cbcd810a1bdc9566bc620f54702d605477155df4a/coverage-7.16.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fb9d92ecfe2d5b494367c67f7446f8b75b68d8d0c8cf3bc3e6997478be25d9e2", size = 265090, upload-time = "2026-09-27T12:28:33.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/b2/cc83f3a6e5789a4e89059c69555bc641c2efcde568405a1c06fc702951ab/coverage-7.16.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb57acff4a74246ae513c142d4b36e18c389c3aed8661914a53f7cd0071031b2", size = 268254, upload-time = "2026-09-27T12:28:35.135Z" }, + { url = "https://files.pythonhosted.org/packages/ac/41/f548c19530f5d66ac6e3c92bbcbc49da7261de3a458b9f3e54a3efb1a0b2/coverage-7.16.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:444889f7f66b74e4455c0a97e0e166dd41177f1dca8c0239a47cff25e05ba7e1", size = 270695, upload-time = "2026-09-27T12:28:36.959Z" }, + { url = "https://files.pythonhosted.org/packages/94/61/4dc27cf82ef96434d2874110ad0cc10ea4621025705dc5049862bd3bd181/coverage-7.16.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a740ea6f083c6db7b926534d159508f80ba275ab35e722522de0d18d0f56e55f", size = 271853, upload-time = "2026-09-27T12:28:38.821Z" }, + { url = "https://files.pythonhosted.org/packages/38/29/bf8072b1b8bd5f2de8b21460a404460b1a2b97e80a9464c78ec0271f6199/coverage-7.16.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8e209591f7c41ae4a9171335cf6156afda0b21de73b02f73f5aa95b2d5fbb08d", size = 265644, upload-time = "2026-09-27T12:28:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/0aecb8721be5cdeb8afd9d6d9f6b463f074e4d8d37f00f4c42442522709f/coverage-7.16.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:396bb16e04ce04efbb3df91456ae4e3da918e69ecdf67fb711b0a0fdf35ccce0", size = 268530, upload-time = "2026-09-27T12:28:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0b/92b4b7628268ee711249958e68fc0328779bd3d9a7ab4715379465aedb84/coverage-7.16.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:9cdf19874e0d247f32f03609200370343c3c7aa260b191d8c2bb251d36198283", size = 265154, upload-time = "2026-09-27T12:28:44.684Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/41c95c1ab29b3dcd357cd1227181d1c98185632aca41ce670ce671b23a43/coverage-7.16.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:fd3d72233eb8b48acc94fa57d44e2d32ce8e7abed02882ccb6d855ccc4ed33ec", size = 269799, upload-time = "2026-09-27T12:28:46.672Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ebeb259aa5362b033a137b86d7274ff4b109d59be8cc9913889b783bf75a/coverage-7.16.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:bb4ffe96aa663cee727659db5a2afeb38c95f8677b747d447b90d6d4874ea2c5", size = 265196, upload-time = "2026-09-27T12:28:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/b2/18/8437620f90d023680a072eee02f968055f3658bbfb7d386d0ea34cfb7f30/coverage-7.16.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dba2edfb054f6d4a08df9d1637c39a5aa3865bca6617c13c86be21e45658a59c", size = 267266, upload-time = "2026-09-27T12:28:51.361Z" }, + { url = "https://files.pythonhosted.org/packages/28/6c/f08e8ee4293e6434035424180bef4d45e028e8ecc006c61bf9453e74405e/coverage-7.16.2-cp315-cp315t-win32.whl", hash = "sha256:251aed777c47c77aba047096d4542889db089227655711dfc2b9c54ef0e15e35", size = 226583, upload-time = "2026-09-27T12:28:53.33Z" }, + { url = "https://files.pythonhosted.org/packages/f7/fd/3f939c2847f4a72c20cff8b1ac33da78ea91a2d38d9b43336e60db719103/coverage-7.16.2-cp315-cp315t-win_amd64.whl", hash = "sha256:2aca0bdfa9e91621d5b09d815357bf63def4fc0e9cb66da67bf2cf93f3b1a6f5", size = 227374, upload-time = "2026-09-27T12:28:55.158Z" }, + { url = "https://files.pythonhosted.org/packages/5a/35/b98cdc354c952402132e675a87f2cc3227fb68f959c84aaa491fbe15933d/coverage-7.16.2-cp315-cp315t-win_arm64.whl", hash = "sha256:b88841e654f09732804809e435b3e005a929ffd9998b872b7b213957b8759cb8", size = 227064, upload-time = "2026-09-27T12:28:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/7a64e1ac90541a8edf50daef0914848011fb057a5bf55284a4811e21939a/coverage-7.16.2-py3-none-any.whl", hash = "sha256:11d28e9123a9156cb405d8d27b44256c9a58fb5decc2073a8f17862057e3aa0f", size = 215754, upload-time = "2026-09-27T12:28:59.075Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "50.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" }, + { url = "https://files.pythonhosted.org/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" }, + { url = "https://files.pythonhosted.org/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" }, + { url = "https://files.pythonhosted.org/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" }, + { url = "https://files.pythonhosted.org/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" }, + { url = "https://files.pythonhosted.org/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" }, + { url = "https://files.pythonhosted.org/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" }, + { url = "https://files.pythonhosted.org/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" }, + { url = "https://files.pythonhosted.org/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" }, + { url = "https://files.pythonhosted.org/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" }, + { url = "https://files.pythonhosted.org/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" }, + { url = "https://files.pythonhosted.org/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" }, + { url = "https://files.pythonhosted.org/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" }, + { url = "https://files.pythonhosted.org/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" }, + { url = "https://files.pythonhosted.org/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" }, + { url = "https://files.pythonhosted.org/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" }, + { url = "https://files.pythonhosted.org/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" }, + { url = "https://files.pythonhosted.org/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" }, + { url = "https://files.pythonhosted.org/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" }, + { url = "https://files.pythonhosted.org/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" }, + { url = "https://files.pythonhosted.org/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" }, + { url = "https://files.pythonhosted.org/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7a/f08d34ce09d60f89ebd391e2ebc6ba2b995e6dd7552f41820f8085f94e53/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67", size = 4716035, upload-time = "2026-09-30T15:29:48.681Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/e18fb65592451a2acb76e9f2fbe14e0f47a8318b4c5430f1633851d03daa/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a", size = 4726917, upload-time = "2026-09-30T15:29:50.608Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/38fdce17e60f6b825e69fc3b7f75e70a6612759980704697e1de4cbfaf6e/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48", size = 4715341, upload-time = "2026-09-30T15:29:52.522Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/d9121a717e0f893c64bd6ca7702614778d7df2a5c309128a002421788516/cryptography-50.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42", size = 4726322, upload-time = "2026-09-30T15:29:54.263Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/e6d153808bf353e152abd2fd4d8f09670d956ac78379ac46e60d7efbf04c/cryptography-50.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81", size = 3873097, upload-time = "2026-09-30T15:29:56.097Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1d/1271f287ff7170ddafc2aad36260c4eec20ccd2fea70f38455e9d56d427b/cryptography-50.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452", size = 3805376, upload-time = "2026-09-30T15:29:58.729Z" }, +] + +[[package]] +name = "didit-sdk" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +auth = [ + { name = "cryptography" }, +] + +[package.dev-dependencies] +dev = [ + { name = "cryptography" }, + { name = "mypy" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "cryptography", marker = "extra == 'auth'", specifier = ">=42" }, + { name = "httpx", specifier = ">=0.27,<1" }, + { name = "pydantic", specifier = ">=2.7,<3" }, + { name = "typing-extensions", specifier = ">=4.10" }, +] +provides-extras = ["auth"] + +[package.metadata.requires-dev] +dev = [ + { name = "cryptography", specifier = ">=42" }, + { name = "mypy", specifier = ">=1.13" }, + { name = "pyright", specifier = ">=1.1.390" }, + { name = "pytest", specifier = ">=8" }, + { name = "pytest-cov", specifier = ">=5" }, + { name = "ruff", specifier = ">=0.8" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "librt" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/f5/9dc696772d241814bacac7880bac32f2930b5a6ebc1f85317b83161a011c/librt-0.16.0.tar.gz", hash = "sha256:ac38d6d8d66bf3d744148dbbc0b8e193e195a51e364ed55e224631f5721891fc", size = 219839, upload-time = "2026-09-29T00:55:32.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/77/900294b3c9a81415323d81b3f3f271a7f5d7ed9d01e824fc38ffa49470b6/librt-0.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:96f576f2711f8519152ec76d0e599243555c1f07679fa73606ca8c8c868c0be6", size = 148668, upload-time = "2026-09-29T00:44:20.282Z" }, + { url = "https://files.pythonhosted.org/packages/55/3b/c301e8cc47758e4518b93e926a0111080155cc79fa03651d202ac2025065/librt-0.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bec3818c7da7c96ceae0ef5915a3d16c52dd08f3ea913bf1fe8568c447c7978", size = 153562, upload-time = "2026-09-29T00:44:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/62/b1/4d5b98114a58bf17e3832cd33400e40f2582f68c2fb7ce25b4468cb1afd3/librt-0.16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c79520414a3fdfc6aabd7593e6169afa14d5f8d9908d4b498db068868b08dd", size = 494337, upload-time = "2026-09-29T00:44:23.096Z" }, + { url = "https://files.pythonhosted.org/packages/88/36/14223c9b0dc38982eb4f399732c8cb27b19c7e027ae0faef5ca51d08d19f/librt-0.16.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f81b5b19ce748ef68d4746656b7929762eb2fe99269b266e4be07e2ee4de7144", size = 485440, upload-time = "2026-09-29T00:44:24.555Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/d7155e6800d0c33e297e529b217649f7361d706491ebf9d2fc0fc627309e/librt-0.16.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c71d1b76210a36729fedfc5115069b50a3d8619054745f8758fe5d6f19e86671", size = 498363, upload-time = "2026-09-29T00:44:26.03Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ce/d5f1fb9f913ac0979d6fc5db9fb9c0bf28cc3470988528957922f15ed40c/librt-0.16.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:473eebc7866bb0a0c8849a292b5e7157c1aba5d14d0f0f610c52158d6d964262", size = 515453, upload-time = "2026-09-29T00:44:27.566Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/ca0fca1a27c26080d1a25b2b31f271e532078a5719878c3daca0c3d4c539/librt-0.16.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4eb1313a19847089ee81e88742abedf285c60538816640b99742d8534b81d26a", size = 509537, upload-time = "2026-09-29T00:44:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c0/26807bf2221a53a2c66f2e23a5c4d57f1c0eaa4afed7cb81d841cf3d1903/librt-0.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebefd60b42e2a82b32d136bb5f7c94eadfcd29f772b547df6f3291d1ed855a1c", size = 532541, upload-time = "2026-09-29T00:44:30.745Z" }, + { url = "https://files.pythonhosted.org/packages/0e/50/c329d509907e1bc061abeec3796f94d255d43ba25d28c3854d2138ed5ab2/librt-0.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6c5da27e8056439f927ea896735da60e616c477a8293feaa3233d4e7781a6726", size = 537059, upload-time = "2026-09-29T00:44:32.102Z" }, + { url = "https://files.pythonhosted.org/packages/84/d6/4571b9c9e65d98e5503734166dce2fd3e216f526dc3c59a3773faa8a60e3/librt-0.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:67e718c7a43f8db325abbbf1404e2d535f12f8f7a1a82259568385cc5274b82a", size = 536614, upload-time = "2026-09-29T00:44:33.653Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d3/a2ed8a292e4ab7889792a9c3f4ad5d1caf6557afce7f6b452e77f8ed20b7/librt-0.16.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:314e703f0c19320dc8094e7a784b9cf29e1b67402515abf580069a6363c0b4f1", size = 517199, upload-time = "2026-09-29T00:44:35.115Z" }, + { url = "https://files.pythonhosted.org/packages/9e/24/276e5218b4b7adb6258a42b7403cb1b7cef25616238d0c2152288a9d2f53/librt-0.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:943c6bbecbdf7fa575a4f2952fcfd848c88ef95507c3fca411e89d4ac3ff8143", size = 558746, upload-time = "2026-09-29T00:44:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5c/9156df81a1726e1efc9567379def2b8777a6d56f464d2f1b760bf951b8af/librt-0.16.0-cp310-cp310-win32.whl", hash = "sha256:7cc365f006891afb006b52d5ee5ee74c09306ffa20e2f8705a32a4450af2f3ba", size = 104424, upload-time = "2026-09-29T00:44:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/0b/62/4fd172cb3f49ab2cf5ef17de519a93cfb935202c1270ba9d44c6a64178c3/librt-0.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:0314058469f4d2fd279ce7c62ac274ac82c3918ef7db62ef0697c4c359370155", size = 124267, upload-time = "2026-09-29T00:44:39.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0e/b04fd2d76b6a78a02286938251b2c119fb36ddece92c76cb4886ef7d44b8/librt-0.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fe4372c52d4849096c6cc1cda2817d293ec51440c890474ed59ef38d46556f18", size = 148058, upload-time = "2026-09-29T00:44:40.903Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fc/e1ed24aadb32d38603c0c07f579d739eb4d90e64e90d525b024e4a999b4d/librt-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c72c5295a84bd249526da9bdca38f2e176d15c31c13bb0063c5053f4ca023421", size = 153073, upload-time = "2026-09-29T00:44:42.165Z" }, + { url = "https://files.pythonhosted.org/packages/be/38/133ab655409420e0c34a9a9836a7f4e5ae75fba758d7fe177099130d58ba/librt-0.16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be56ba9c884143495b517f23fe794ae367d58cd89ea0fdd6d437e3c024a87f9f", size = 493052, upload-time = "2026-09-29T00:44:44.043Z" }, + { url = "https://files.pythonhosted.org/packages/94/42/aaa663bc9421f5bf7bbbe9d3316d7219749698ec7688ac3b519770a89c04/librt-0.16.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:ef46c1a29ffb8c72e882e22618ec618778eacd0578fb22c6e7cf9c11d15f357b", size = 485506, upload-time = "2026-09-29T00:44:45.479Z" }, + { url = "https://files.pythonhosted.org/packages/7b/59/83a6eb5087ba8b45b083859e2085256963e06d4c2531944cdd962670bfb5/librt-0.16.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d3c94211ee0c4f8d649ec06b7c115c0ec4eadb873a0e3154ca15cef3f814b071", size = 498708, upload-time = "2026-09-29T00:44:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/15a8d62b2bc12935080ec7277e2e0b9b233047781eb784a51d3e25af669a/librt-0.16.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94aed6a8308818b91677957d1bd03188869cd7aeb23c5dba7912a6c0402f7602", size = 515981, upload-time = "2026-09-29T00:44:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/4f/287cc282ac5dd815204c0779e79401bb0a568c3a1335fb319bf0838fb566/librt-0.16.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:349c0bcb87ebd07481b6ff781e25cdc699723dbe2212e57dabb27f7a13b7b87d", size = 508670, upload-time = "2026-09-29T00:44:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5b/ab7292add898ade0d2b5b75d32a923aae315861671aa5c39f5491309f7ae/librt-0.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:001bfd59a7d45b17e3e75f2a8c6405280b35e7b84471792778e718c4f368950e", size = 530393, upload-time = "2026-09-29T00:44:51.356Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9e/8e6bcf027d4bcbc16a4e864b3d158a1d3e5f55ffa53af0238020cad89271/librt-0.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:28e038895b998d7a0c7798922ce8a1dc157675df5cf1c9ef0aca809ed804b7a1", size = 534302, upload-time = "2026-09-29T00:44:52.893Z" }, + { url = "https://files.pythonhosted.org/packages/96/a4/8c5ab4f3b3a8e905ff8305d76976a2e666f52fe049edd398aed2402beb36/librt-0.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0dbe4096a7ecc00fa835d24510ad8545a4efef738dac96e0e63516783ccde905", size = 536150, upload-time = "2026-09-29T00:44:54.421Z" }, + { url = "https://files.pythonhosted.org/packages/ea/5e/7c4bc267e3cd4b8bd288de9f62d61fff203829c9cc649ea2aa12e9218cf0/librt-0.16.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5cd5b092441053364af968ea12084692cb9d4a22f3ce9524e377880bf028761e", size = 514316, upload-time = "2026-09-29T00:44:55.998Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/c8bc6b50f43c73819334233f5a3259c81216742cef56d2140c3dd377c3f0/librt-0.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ddeb3c9dedb461bb457c6c7d9aa7fbf35329da313d1a7543d00c8d0f3473c96", size = 557731, upload-time = "2026-09-29T00:44:57.581Z" }, + { url = "https://files.pythonhosted.org/packages/93/fb/e241f91bc8de6c7ac1c6a20ea6dd381bc52312b44d711ffcd97140db1e57/librt-0.16.0-cp311-cp311-win32.whl", hash = "sha256:e05108e0849966f53a8d2d3112a7af881d0efaa479bc735bba91108f9f2350a7", size = 104922, upload-time = "2026-09-29T00:44:59.078Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/c892383d2bb1a6a80b2ec8ede84970a265a792c56614a06282bf3008517c/librt-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5f49cff01bd608ef7d97104cb035c75455e79c2d70bf4a506cf773338ac1860d", size = 125093, upload-time = "2026-09-29T00:49:01.621Z" }, + { url = "https://files.pythonhosted.org/packages/e4/56/8bbde1ed0b21480163617648f638fdaec9374cf5e9c846ebcee34ec02a42/librt-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:1d28ae980ae2218f9c5b95d191e947296f918c9bf0b400d467a9430275bbe678", size = 115910, upload-time = "2026-09-29T00:52:40.131Z" }, + { url = "https://files.pythonhosted.org/packages/ad/76/bbdaeb87b7c47b5c7343e90222b9bfa4e4a8a83f647e02933ad0129225b1/librt-0.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe52bf4641069e7978a14253b036cb9002def1926317e710f2e249f8a8c47742", size = 151028, upload-time = "2026-09-29T00:52:41.508Z" }, + { url = "https://files.pythonhosted.org/packages/fd/0c/ab8ed3dab0085931aec4a792c7eaac8dc6c5ff4691fda3a5360d9d8a9cd2/librt-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5bcc2c4726ced915b00de0c9856a4eeabfb3fddb93e10e0b8f735b7709358b6d", size = 155283, upload-time = "2026-09-29T00:52:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/eb/36/494e79d460c80c1f030661e8287c9eca5e1ad652dc2b2180b6cd42abce0a/librt-0.16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff7baa55f8e7c69851419e50a666015d02a74198716fd45c0125a2112e0a389f", size = 503185, upload-time = "2026-09-29T00:52:44.638Z" }, + { url = "https://files.pythonhosted.org/packages/9b/34/a8464038dd9db6e4381fa2b6eb73dc9a50888d77102c4c139304ac35cddc/librt-0.16.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:b95d5d92ab83d39e760a52091bb1baba664f3a2351e39b1e16801e5747c2f0e9", size = 496738, upload-time = "2026-09-29T00:52:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e0e5e334ef0c6ed27039d323819368b9ef6712be87d55ee2bf9799398afd/librt-0.16.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6d085d70bce51d43c5c7c36d63490770180d8779e71c49305c87b4213918de7", size = 513818, upload-time = "2026-09-29T00:52:48.068Z" }, + { url = "https://files.pythonhosted.org/packages/ad/f7/7ce72cbf19d0addd05090b339152fd0548a02562c2866a603e6e3b3da2df/librt-0.16.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36e53948e99bbe3ffea257124cfcae1cfb01831555c9a9c903c9f9a72db7fd07", size = 531952, upload-time = "2026-09-29T00:52:49.816Z" }, + { url = "https://files.pythonhosted.org/packages/83/22/0b1bcb6a8e723c8b4fd60dfc8ae8ec6461c54073fbc8685efeb8d900d407/librt-0.16.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:54d11f726aae9df5a6ffbbf0a03a52449bbac84a53ef03669cb41cdfd4ae41bf", size = 524465, upload-time = "2026-09-29T00:52:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/64/2e/e9c23b8b9df1813da1be205deca9606beb7ddd033972246cd426d05374a0/librt-0.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4323193ac0cd025f85af531df8ba91bf24d1973b401697347a6282e8fd3fcf5e", size = 543191, upload-time = "2026-09-29T00:52:53.284Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e5/6a8b21b342c03ed7e230fa3afbfd2edc58e6e87ef1f0d11fa2b9a748c252/librt-0.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e42f8e098b9c5396fefa05fb1cc7e33b0e08fc51da106b5de4a45fd22aac6743", size = 546979, upload-time = "2026-09-29T00:52:54.932Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/b5129023eced1be01e01c22757f53be551d463b1bb7264f787927404c1d6/librt-0.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:39ec1d5a14e37baf1450a6cabf03fe552340808bf1ad9d71824ab90117716459", size = 555317, upload-time = "2026-09-29T00:52:56.869Z" }, + { url = "https://files.pythonhosted.org/packages/71/89/28bba5938c725fe91f06bf93f7fa6c6b150229df53a87b454b0d5c2a796e/librt-0.16.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d1aabe3925cbb4a08d15b7b20ba4011b53019da0c4173a25155139b7b1baed65", size = 535846, upload-time = "2026-09-29T00:52:58.475Z" }, + { url = "https://files.pythonhosted.org/packages/22/92/63773026614f888c5d4e370e395ce42ca604b89f70b3acdfedbf94851b80/librt-0.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:300c3ffdc459f4a779a8411ecb188e3ac0b1ff3a3a7b099642555dedae06c69b", size = 573700, upload-time = "2026-09-29T00:53:00.127Z" }, + { url = "https://files.pythonhosted.org/packages/66/8f/347d4677eefb57cd9f8e01d95a1dcee9a91b4e44e664173c32ff6f3752e5/librt-0.16.0-cp312-cp312-win32.whl", hash = "sha256:c17194318e4c0c0348b36f36c2ec7534436fe0a4c15582403162a4f08c80797a", size = 106158, upload-time = "2026-09-29T00:53:02.031Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2c/5193dc81127cd5ddfad031391b046bf32dda219b38463ab872407ca30646/librt-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:25a58a19ea8d83b68209f04912df765e9260635ef77646542ed4b4abe6bc7940", size = 126176, upload-time = "2026-09-29T00:53:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/ff/3d/9668a400c8dd81d162eba38b33fa49fa6205f1a64493570a13fbb815c3ee/librt-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7be7cf555bc30ec12622e9447299cc4a9b8ff307548b634794353db0c2065dc", size = 116572, upload-time = "2026-09-29T00:53:04.815Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/ae5e0e9dba45d1399aa04a5395bcc0bead40d9fa06dc903634a7b4d7473d/librt-0.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5e6144e68b577f157519f2ba88ca20e3ed61c29b00e5cdfa76cd2d45acf059a", size = 151036, upload-time = "2026-09-29T00:53:06.284Z" }, + { url = "https://files.pythonhosted.org/packages/41/5a/48a16e323c5f9447a94cce7b59babf60fa04e62c3365ecf060c77ed8b320/librt-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:33f41443a1f4e1f099331b3d8120e409fbff84b9760bc1cc9ea496f37ddaa5cc", size = 155211, upload-time = "2026-09-29T00:53:07.72Z" }, + { url = "https://files.pythonhosted.org/packages/3f/29/0f59299eb4251a409b2e690ad4b7d9f8a676db829d7817ec961f32b44f7e/librt-0.16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e510b7770bee609617a3374a96548eb114cae048023e3f049ee449e7ff2db32", size = 502587, upload-time = "2026-09-29T00:53:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/de/ba/d6fb4ef8d1537c396079d72289f16be7cd35a366e5065c51253fea2760b6/librt-0.16.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:efc49c462d4516b8a58b00b490078fa64689fd1fe66970cc190131d7afb8027e", size = 496146, upload-time = "2026-09-29T00:53:11.016Z" }, + { url = "https://files.pythonhosted.org/packages/52/fc/8c50dd4d7cc97c0ee8f252c8a3104980f234391cf1519b554e8b9de08b60/librt-0.16.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:92caf82ebef5e12d21c72242b70d1e92536f1711cf2a727a4c276de4b4469087", size = 513336, upload-time = "2026-09-29T00:53:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/a9/59/16c409c56f708eda2db9a0553662845d45e3871c77d70a240dae3f3bdc56/librt-0.16.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17bac7f7a16b328fff77e440287693eb017abde913595b5827ebccbc21ecd8a6", size = 531689, upload-time = "2026-09-29T00:53:14.39Z" }, + { url = "https://files.pythonhosted.org/packages/88/82/d34772a6c29d1446dcca6e64d74062efd508523ab351aa16625a4689d5cc/librt-0.16.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5b976054553670829985ed767feb78fb6bcede0175327c4844dd5c281c1be659", size = 524537, upload-time = "2026-09-29T00:53:16.064Z" }, + { url = "https://files.pythonhosted.org/packages/77/8f/24c5631313746131ccee53bc91fdc8374f9cf25e0082a1fee9c93bb98acc/librt-0.16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0058f9d68721094105917254c72ac0569117bb7b13b9769cf45d26d89f9d21cd", size = 543231, upload-time = "2026-09-29T00:53:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/f2/cb/5f8e0d41dbd8b499c2265e939c31acc9ba59845565bf99539ad1c06aebcf/librt-0.16.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b7beaf3f4487b7d8adef1f158b49067cb4d5a19fa7a3bf31a4e7a820e435c5", size = 546499, upload-time = "2026-09-29T00:53:19.666Z" }, + { url = "https://files.pythonhosted.org/packages/be/61/063052de441d1385f59cea4223f184bf9e5d125de1ae3239b490aa1e486e/librt-0.16.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:468df902df016a06eb0e40b0747dc8d14e47d7a38b18b63b1fb167d85cb94d63", size = 555261, upload-time = "2026-09-29T00:53:21.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/11/a2ada0529372268d6401afa9d457a095b68cd7753532b6f7f33049a19b43/librt-0.16.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:aea7b1f2b125dad5de85f049136651bff256c883c65e6b9209b2da0a1ac3cdef", size = 536029, upload-time = "2026-09-29T00:53:23.07Z" }, + { url = "https://files.pythonhosted.org/packages/23/9d/5bb6d38853382986dca702fc7e06c8256d30f5fa0676d882773b744610dc/librt-0.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a8afb6557920860b7a3a596eb804cf37e09e7cf8a803db2478c202acc72d8c2e", size = 573748, upload-time = "2026-09-29T00:53:24.696Z" }, + { url = "https://files.pythonhosted.org/packages/99/f6/0025cde35ff7f607684dc775a2b2d732cce2561cec050a6ee1fb2e1fc6fe/librt-0.16.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:77c7a2b4fe2c1369e0d5aa1cade26740a7b14be32fbc9a5535d617d20065c39d", size = 79861, upload-time = "2026-09-29T00:53:26.089Z" }, + { url = "https://files.pythonhosted.org/packages/93/93/303b8592909bd583f83f02818ffbea3f7647ca1e22b1cd5465d04b8145fe/librt-0.16.0-cp313-cp313-win32.whl", hash = "sha256:02d89c813d5ff74b17df72d3a34819d132cd168e56b81bf755b809bd9e46b8c4", size = 106236, upload-time = "2026-09-29T00:53:27.43Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/80bbb463c60ed18e29cb26aba386ddb76609580e4e7160710e550587018c/librt-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:14ed6ebe3e4f85f326d7920011ad30ff49ed9334e62cf88caef9ba973d9e3a92", size = 126180, upload-time = "2026-09-29T00:53:28.7Z" }, + { url = "https://files.pythonhosted.org/packages/43/80/b1a6fbdd7da825cdd55c71aa81eb6cfa82c513c360774152eacb50b4a771/librt-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:83d4041a3d9b2fd053a8a4e1f22878b3e5833e2712956382d5c048d791454e91", size = 116557, upload-time = "2026-09-29T00:53:30.012Z" }, + { url = "https://files.pythonhosted.org/packages/1e/93/9e0cf7da129a93c3dc7f45bc3cd4a660f2aaa995aa8a6f95c2583ef41239/librt-0.16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:931a0bb0fcac88f263e269e46eb30ba8e21402cd3c62ca40cb97034c0693fab1", size = 149844, upload-time = "2026-09-29T00:53:31.391Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/8a90d2a8f2b2e471bb486b7aec117b8ae622715ed6c39853aec48ea20073/librt-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1bc17e54e5305f8d40b7ca203671ff5a9e59c1d0f8ea0f625dcca53a3984de11", size = 154111, upload-time = "2026-09-29T00:53:32.718Z" }, + { url = "https://files.pythonhosted.org/packages/35/ce/67abb46258da4d3e42ff5b141db6f38c59357bef84c7979f183b22f924f1/librt-0.16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:877698bf6bca5721d8be345f2fe09778e40ecadea8b58c73075f2b1a53666bf2", size = 494234, upload-time = "2026-09-29T00:53:34.466Z" }, + { url = "https://files.pythonhosted.org/packages/12/f9/ea7162414a16f8f1bbd3b493ad6d22b926c5471916e078350bdbca8c4e5f/librt-0.16.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5981c011b306781ce561e18e14230a14524a3d8109b97553666c942c18f31a96", size = 491173, upload-time = "2026-09-29T00:53:36.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/09/9b3e869060dd33f9989b80ba4fea306f6db8cecefcdfe6d7346ef0603f65/librt-0.16.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:afced3dfc17cd805ecf7a3d77996a71cf5f2c75aa66eb0c21a9930f4fc992f86", size = 505547, upload-time = "2026-09-29T00:53:37.686Z" }, + { url = "https://files.pythonhosted.org/packages/50/07/79007d2165f649ea93e08c0962d1d62c70af9cde77965255095bf9d96f9a/librt-0.16.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca8052401c55d7511dda6760719fda7618067e83535d7d0010096d216c34b667", size = 523057, upload-time = "2026-09-29T00:53:39.301Z" }, + { url = "https://files.pythonhosted.org/packages/61/0c/8fbaff66d0ba376d8864653f5acce1569bae27e89648671f26f7eca67ab8/librt-0.16.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1e511762a074005bb0aa569166779834e75e438370226930d0ce1866d4b6a33b", size = 515158, upload-time = "2026-09-29T00:53:41.012Z" }, + { url = "https://files.pythonhosted.org/packages/df/2e/23ff0dece76f07a4124413a57682efa0bbeb5765ac0122bc0955513f82ca/librt-0.16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1e8591bd8a5a628cd7f07954c6a1592359a878bf032957a8e9057a41d644311", size = 534183, upload-time = "2026-09-29T00:53:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/e11dea21d9a29486eba78887380374189d472734fa32cc50cb37ca44d3d0/librt-0.16.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4aaefb4ba6c07e1aeebb2795c8958148f1d6f9af3b555b53d23d766edb6d67a", size = 540663, upload-time = "2026-09-29T00:53:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/75/e873ae158a8b7f5359be33e7fd6c1fbe02d9a78a3e89a77de6b0e837f476/librt-0.16.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d92db7a0f6aee44f1baee94750457e8d2d1c6ccea41842de6268d34e8dc7eddd", size = 545908, upload-time = "2026-09-29T00:53:45.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/36/8939d3f6a93e11bd9592e6fe28d2b44f1c2dc4bed6e22e72359a91e18ffb/librt-0.16.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:378dfaffb38e59c24a87cde5713cd865d51ff7383fa12947f3907f306ea1ca55", size = 523302, upload-time = "2026-09-29T00:53:47.596Z" }, + { url = "https://files.pythonhosted.org/packages/71/14/35309f44a077f0f42ade0e2e7cd88c0cea760c661af205c01ea90d3c0e1f/librt-0.16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e0c39bdc85370422e8b637be76eb1fd07d30967551b03e62267dd156f553152", size = 565911, upload-time = "2026-09-29T00:53:49.278Z" }, + { url = "https://files.pythonhosted.org/packages/78/0c/df6255b94967f3159ebc46f08d8e783c12da6ee269ddb74b2efed63c640c/librt-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:1b384b90ab79a7bc30b566895809a636e0666f21f3cf12b54823d025b7e83839", size = 79119, upload-time = "2026-09-29T00:53:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/6b/44/d30d5a5461378c9d33f36736c6791c3b4c4ba4b1ffe0da7350aedcb2c9a0/librt-0.16.0-cp314-cp314-win32.whl", hash = "sha256:52327da75a94012e7f932f913d20d3876bed3c102be00e6c3e8600ff7bdd58a7", size = 100140, upload-time = "2026-09-29T00:53:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/769712f356a1e897df3581bb0c3100375d054a000de26099360ea65b5111/librt-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:3f0b8114c44b2ac06ff5dacd08e07e8e807ff4f46083f2a1602685122559be41", size = 120464, upload-time = "2026-09-29T00:53:53.495Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d3/ae2abccc8bdc8b063c1613a77686e17b74e9d3d60cfe6f12c63fa2821b9f/librt-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:8caf96a4ef8fb27d0ac0d1ad8337d26a240acd4a02fe4345d0a8f264753e8f99", size = 110889, upload-time = "2026-09-29T00:53:54.817Z" }, + { url = "https://files.pythonhosted.org/packages/e4/26/0737d4be058dd6376eade7dd8b380d4b869b6394cee929393a5a431c45bb/librt-0.16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:953107e2f68d0f3512c48f898b0dbf0ce5cc52bba0f318d847c985dc555ee4cc", size = 159838, upload-time = "2026-09-29T00:53:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/01/96/9bc96531d7c620e9949af904470f02e3fa8f35129ab8e8f281c51eaa3788/librt-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad37d5b9abd49c9a655dcda7ea52a8a752884062ef1ee71ae17c2f2a0f81fe6a", size = 162018, upload-time = "2026-09-29T00:53:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fa/b0289dcb186eb3f97221ba00da5f4bd3ba7fa5e99752d48aa8d336615334/librt-0.16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c4aa329c17bd1aaea4f6e89335d8ccd494b3a5830b6654462273e50e11023f0", size = 703983, upload-time = "2026-09-29T00:53:59.024Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ab/05ebbbde7530fc5eeb58fbd1581522a9f64f132fa703c4c595eed6e14760/librt-0.16.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0ead24d2562a49473dddd9efef8581f020007eb0054389c3ee3ffad38b1ca4c9", size = 683847, upload-time = "2026-09-29T00:54:00.671Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/f52aeecd4dbadbddf80725ba7de126d8bd5d0eb66247eae17a81ed90dd4d/librt-0.16.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02118f56a9c36ddd07dfd9b919d9ecc117ba20a90987d56aa4c429fa34509188", size = 698045, upload-time = "2026-09-29T00:54:02.247Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/fa277a835cd6eb42380591ceb85f48c5b4d2b2d7e2cb9869e1e17d24d237/librt-0.16.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e29522c62e28595ff7e324c6834ade51127707f0e255b18d1c1cf03d39c1048", size = 724640, upload-time = "2026-09-29T00:54:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/25/e4/2cf64354f3fde8ebd591b3b48f96510bee24ac5f7d1e567adbbe210abdbd/librt-0.16.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ff4b2367926b69c6215635902cccb04048e73094e9862900d27cb2c6bbff143", size = 731748, upload-time = "2026-09-29T00:54:05.741Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c9/c180af3e94e01aa529fa93d7733fec2abc47f222345400cf21d5481d5f8a/librt-0.16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c6f1b27bf1632a7e016af9f145f82be95e1edd7721a646505c21059257cb5a04", size = 754960, upload-time = "2026-09-29T00:54:07.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/d6/01073aa78c58f356b10d9c57b3fe9abb143df338142bcd316df417b89db0/librt-0.16.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5696d7f52e7b37217cb3a8f92c744fe835942602fdd4c1a8bc4741d3bfdce15e", size = 747000, upload-time = "2026-09-29T00:54:09.06Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d8/1de3783908658d697a8cfc00582f61299ffba7796d7260c112e4700b1109/librt-0.16.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:6072e92dd876ff6ceeb6cf371e35e51f479349837391341f479b08df4564242b", size = 749569, upload-time = "2026-09-29T00:54:10.702Z" }, + { url = "https://files.pythonhosted.org/packages/b7/32/e817f66c96d6caa8bb8435ff93c4c220624d59efc506be59ab98fcd01d0c/librt-0.16.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:39ca4f2f2fe05de8e63493da592d84311adabe5bef52b193851981da9816b302", size = 729483, upload-time = "2026-09-29T00:54:12.25Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c9/23992ccd2b9d22798fdd0f61353183a47414e651da782ad83eb680f50833/librt-0.16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9807485a908f00355820f18e91e045ffdcdc5adb68aaec40a1e2b88c5f7bba1", size = 776584, upload-time = "2026-09-29T00:54:13.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/3b/e8af957f08e6e2e8e566b099e43d2748418aa565df6ee1d9d3331209fdfe/librt-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:94be5cb7bca4df6201f4183e9e4fa2086c655283d20b38cd84500a69057575a7", size = 104402, upload-time = "2026-09-29T00:54:15.568Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/946e6443d7cd32347a086043395e421e52fd603c9163d2ec970ceab8eed6/librt-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d46ca272b251d033dd4527b0dec5f261a28a52bd5fa0f99c117b0a1f8588cc2d", size = 126227, upload-time = "2026-09-29T00:54:17.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a3/bc4f9959d3c62bcbf9e5fd3470a8bbd8bf33224ed4c25d7fba173201b8ac/librt-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b9d6d4b14e92d876f8026b54c20c445f36425214c1081dc76f74e40db386b82b", size = 116493, upload-time = "2026-09-29T00:54:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4b/10fdb42dfab4c1533e1570e686b18e86ff4328b406361b39fb3016667638/librt-0.16.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:6fe436af2eaf630474f491af5d032cbe45f93fcff5c3b9fe4ab194a7255b20ff", size = 149829, upload-time = "2026-09-29T00:54:19.933Z" }, + { url = "https://files.pythonhosted.org/packages/8e/30/a90ca13f1d3d91af1680000a4038536907018fbd51764767287a05d28b8b/librt-0.16.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:8ff5d26c529336be9bd7ae04483235d77778ee7d6444a95353102b542601ce81", size = 154522, upload-time = "2026-09-29T00:54:21.306Z" }, + { url = "https://files.pythonhosted.org/packages/5c/dd/bcf364eacfa070bb1fc88d503111ae7177914197ba1fa717c748926e7930/librt-0.16.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:909d8e3c1faee44cb762b1c519ff8613dcc5ceae5c99987a00917b5a31fd1d6a", size = 497593, upload-time = "2026-09-29T00:54:22.798Z" }, + { url = "https://files.pythonhosted.org/packages/18/c1/2c4e81e347bdabfe8346bfc6dc37ae5e154d61c88e30848ec0606025a5e0/librt-0.16.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:6d4a64283ee61824b5790de882bc68e2d9d7a5143537cb7a966f7354f71646d4", size = 480459, upload-time = "2026-09-29T00:54:24.364Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d7/fef2a3cb8400701be496f6e459876f650b3451f407e30cb243de571ae615/librt-0.16.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5810ba811297fdf37a1531a57667cb8ace0842013ca8606bf9eb7c24cf4be154", size = 507814, upload-time = "2026-09-29T00:54:25.997Z" }, + { url = "https://files.pythonhosted.org/packages/e1/6f/53762927a32e9dc9eb1d1c1f3528da281290c86d96929a8af652f671266e/librt-0.16.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e56aaf8c167548dc8e5d6f3bd0f48dcdd299a23c73be3f744aab79d99e9c7f5d", size = 525114, upload-time = "2026-09-29T00:54:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/6c/67/0b9d031f303c4e8c691a9a8ef9d272f13b530c11cc563b819df621b6a348/librt-0.16.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f36c58e33b304b525c6c9c5076399c6ebf1109e17b9051a05a407b091b9215b", size = 520465, upload-time = "2026-09-29T00:54:29.331Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/2056a3a85864e882eb17a203a10ddb26fa748bc9718ec67e79059ab46cae/librt-0.16.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:242e00b3d4fa37c3d3c1ca5f5c9adb7d909ddb1eac9c41f2787320d00caa0af2", size = 537257, upload-time = "2026-09-29T00:54:30.915Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/e0d79790c163cbc0909e209a6f62e30bb713b647f905a176cdc64848fd4d/librt-0.16.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:0253721561787b8df8443eb347b7a6461015354e5bdd37ee38a41fef220d2bb0", size = 527442, upload-time = "2026-09-29T00:54:32.478Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/1c0c95aba7af51f4752ea34fbf2eb79b36e7cae3a72536248e8c735de735/librt-0.16.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:3e483a8d69ede8067db70c0e83007423b6925de6fd53afed01d66160f2e9398c", size = 548139, upload-time = "2026-09-29T00:54:34.183Z" }, + { url = "https://files.pythonhosted.org/packages/98/91/a8a43dd5138d4f55f88846b8f0c85454a4fad69952cebfcff9948f831290/librt-0.16.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:69ba927445cfaaffb4081003ef5224c55a5c2ab67ef956f416ef744916e44121", size = 529708, upload-time = "2026-09-29T00:54:35.761Z" }, + { url = "https://files.pythonhosted.org/packages/2d/41/d5226881ab2b7c20d9d587b37bdd4a0ec8775a96d00ca87ac9f385587db4/librt-0.16.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:d6a365f2ab45a984d0e00eee0dd17f599ceab8cadab6ea07b6111c8132fc0e42", size = 567781, upload-time = "2026-09-29T00:54:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/bb/bf/2345ba57a626e8c78c4ddcc724636a8df5a593fe46c1ee76bbf477e32b0d/librt-0.16.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f01f3805f2dae4781c0c34b440e31740d082950bdaf89a6f601ad589a28af57a", size = 79177, upload-time = "2026-09-29T00:54:38.792Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/99e77936cc9207f629b077bf7cbc5e2f0827cddd7c29792f04ef881bd2c3/librt-0.16.0-cp315-cp315-win32.whl", hash = "sha256:b0e3e721c75d2e79a76d4422c79d7ba705fe1bbafec907037fe7a657a480a0e3", size = 100127, upload-time = "2026-09-29T00:54:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/56/1e/801fe26bc622061b9dfd010e166d94142cb774b733217e6d98d1c0cf2638/librt-0.16.0-cp315-cp315-win_amd64.whl", hash = "sha256:bc02954b1295de798bbdb0b4e2d8a28c2117de8b5c73dcbeb27dc32572dfb971", size = 120480, upload-time = "2026-09-29T00:54:41.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a9/d533983055bd36e112627384c2c038845d1df882540b8bcefb566475640b/librt-0.16.0-cp315-cp315-win_arm64.whl", hash = "sha256:c5db585d43449a5f54303d4b2774e45e1babd975cfe1630a3d708c0b80c3e560", size = 110915, upload-time = "2026-09-29T00:54:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/55/fe/d62238fa9c653b0e0613290349467cb65711b5800e8e474f45e942a0ad95/librt-0.16.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f06c689cb14afd9b612727553a5ec5a40febf113ca41c4413a2b0b334285884b", size = 159714, upload-time = "2026-09-29T00:54:44.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ac/f31efe7818700be72ba4f9af8a80fa67c39808c8d26dacc55dc1f6f35172/librt-0.16.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:13b4e8aba90b0b1c82474e9844aa9ffe7ad3faa484350e1da64cb8188d903134", size = 162086, upload-time = "2026-09-29T00:54:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/d9/16/4d7487bf86a9d7e8e18f37ba5538789233ea693637379b75701bd35ec9de/librt-0.16.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a269c46ae327d8e6f8c1f85f7516cb52c0fa48127565a1105a4f4a05ff2a0b4", size = 712727, upload-time = "2026-09-29T00:54:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/77/74/50cd550ccc1a517b9ed62347625c01ddf8c4afad66490312677c011458f6/librt-0.16.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a33e0dae1f8592146a4764d54ce842b278732d21a84e17c3bbe6b1bc158a2248", size = 681106, upload-time = "2026-09-29T00:54:49.126Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/7293f712975ee6fdd2251411fd9ef1c62bc99c7b83ecbe62dbc999b1fab7/librt-0.16.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:47ada6ea32636492c61aa8ad27ae3b9404bfe7a97e3ba946d1984236cc741da0", size = 705921, upload-time = "2026-09-29T00:54:50.905Z" }, + { url = "https://files.pythonhosted.org/packages/67/f7/8aab946f11d59d1bece9ffc65d994b200c789a8ca5a95c17e17e609f912a/librt-0.16.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43bd6e642d8a248c114327f98dd25ac5a7cb5aa168ef02f0559b91874df16b8", size = 731786, upload-time = "2026-09-29T00:54:52.584Z" }, + { url = "https://files.pythonhosted.org/packages/43/76/1c42ab31e7cb8384ebf6d3af607213c495222474f4940443ae7639ab7685/librt-0.16.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c3d1bb7841a816ace6449bb26d3f9560dbfa20e71c568d23f0f62bf1e68f50b1", size = 744964, upload-time = "2026-09-29T00:54:54.217Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/4b19982d933d2dfced671e840b219e4c1cd3f507df6e6dabb51bbd4e3850/librt-0.16.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:3931f7a3db322e7f44e02a280e3949326ce9579ad388ee8d691dc7c76da9fb70", size = 765305, upload-time = "2026-09-29T00:54:55.815Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1b/e872583de2dcb3ac7746e7a2321aeeb168274f3952a87dc66e05ddb29faf/librt-0.16.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:f4462528b6000afe8f16907b5c7c2553abf1df005ba5140e6eb394541c3624c3", size = 744959, upload-time = "2026-09-29T00:54:57.532Z" }, + { url = "https://files.pythonhosted.org/packages/10/de/a18c6bcfb297af2674233e90b3c3661c3a0af0f1434a0c966d2ef708a835/librt-0.16.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:80039ba9b6a7d5f1a0175a4cca6bbefead87bd854c80abad1cb30afe47a830db", size = 758138, upload-time = "2026-09-29T00:54:59.484Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/0df1d732539c297bb1a093e1fe3204d2faf76a9022cf4e1d1c2fce059790/librt-0.16.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:7a1d272724b581bb6bc769dfdafed6da2ecc9886ba2450311de55a4ac2e1e9cd", size = 744268, upload-time = "2026-09-29T00:55:01.179Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f7/ccaf31331f20c91a5bd9bd48ffc3f743f9c81bb5720cc7b2a720ca204f0a/librt-0.16.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:fbe4fb8c5445f7496d7f7f6bb0807875d09d47e6771ffa175fb2df2895fb86ba", size = 785783, upload-time = "2026-09-29T00:55:02.938Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ea/7421d9e6db894cd6cd788b604b3394a163ded6b942052b6f96c23ccf08e1/librt-0.16.0-cp315-cp315t-win32.whl", hash = "sha256:375bfe6b572a8f6cfc398709356046173bf27e64c4c5edaf5f7062f051fb4bf9", size = 104355, upload-time = "2026-09-29T00:55:04.533Z" }, + { url = "https://files.pythonhosted.org/packages/85/6d/7c31a506eb847bc58aeb2402d58e17605e821f68ab5df5e66fe274a6df41/librt-0.16.0-cp315-cp315t-win_amd64.whl", hash = "sha256:bd3150023d3dc2bc70f3784e59ffa1140d56ddba3d8125b3d6f9f85221279bfc", size = 126134, upload-time = "2026-09-29T00:55:06.017Z" }, + { url = "https://files.pythonhosted.org/packages/36/69/7a5d10ac409c4da0355e054a14371871da9b5557fcc42772cd00181c6cce/librt-0.16.0-cp315-cp315t-win_arm64.whl", hash = "sha256:8ceafb70f2a4f0826f11031942e59c0728fd98da112dc346d4352bde1e486866", size = 116391, upload-time = "2026-09-29T00:55:07.484Z" }, +] + +[[package]] +name = "mypy" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/4e/64300736cf0a0373a27b94a91b664ee7382e36f77b0621bae6381da3e180/mypy-2.4.0.tar.gz", hash = "sha256:77bdaebd452f43fcfc4cc3ba94352a3ea537cd01e3f2d0879f48673d2ec00d6e", size = 4064425, upload-time = "2026-10-01T20:40:39.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/78c9ac93933e2ecd51d807021f78c03ffe98640f33951a8be159f5fd55bc/mypy-2.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffda5244fd1ad71a1e54405e35f50d09b80c3978efa120f58bd1252ae32c62d2", size = 14408618, upload-time = "2026-10-01T20:39:46.766Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4f/5f5f4429fd5a4218ce6268164a66afeed75c7e2ce5522a4d163faaa5fe85/mypy-2.4.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:615b03922d40e186fd1df73156473db0ba525c639bb4fd1cf28e1694879c9b23", size = 14552749, upload-time = "2026-10-01T20:40:08.325Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/fa1c319cd51db7a6027b46117460d5a361c6bba0c0f0f0670a2dab8ce2a5/mypy-2.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13fa24f439c0e48a290a3922fa14ccd22f2762bae99d2142931b3e40a9055080", size = 15505846, upload-time = "2026-10-01T20:39:15.475Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/14c02690776fb04e9fa60b81173cb9ae6315e0745fad3813b303737f2f0c/mypy-2.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3adef556a19eb3b630bf86a79c29d7da3d61e541472d0d897ca01b171c0abf8f", size = 15823059, upload-time = "2026-10-01T20:39:35.429Z" }, + { url = "https://files.pythonhosted.org/packages/76/d8/d29afa819f8cb765018d6e2773e390a41ea4592f75abe417135969646857/mypy-2.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:1dc0f64b0a92ae27a49d2175f0bacfa56e15bdc5b420cf92e0c1f79292219cbb", size = 11312927, upload-time = "2026-10-01T20:39:28.509Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c8/966dec4d80303855d824e6bf67fe1da812d88adfe4d75e24d8545dab4826/mypy-2.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a378fc15fb33e321f04652c166ce73eeb8833a97c3d218132844e938cd93220", size = 10879252, upload-time = "2026-10-01T20:38:41.545Z" }, + { url = "https://files.pythonhosted.org/packages/26/67/e53965e67d23d48e78dd603aa7525a8d77e7818c23954cc217ccde5cd5c0/mypy-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d20e6c7c35fcbf2a0ebdd0eaeacfbc243009dfd33ab7822d54e213912e6dbbd", size = 14315745, upload-time = "2026-10-01T20:39:25.03Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5d/a588ccd57625822d8484d7aff4e9c70f88d042a91c2ec84324a2bc14b3f3/mypy-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3011537be6cf1de4511c0255a324362a812b58184bbe61e15f59c8b31033bd74", size = 14465652, upload-time = "2026-10-01T20:38:57.19Z" }, + { url = "https://files.pythonhosted.org/packages/05/af/b4978a5566cb829ca563dc08e3ad8d944f116078786ad54ba3dff2bcb64d/mypy-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a96b07a49b7b1d025ce59c1b3acbcf24bead9a83da4523c4a6bde1bb94e7a0e1", size = 15419180, upload-time = "2026-10-01T20:40:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8c/52c71f31c54d0a749d832cc73b933c6e38c52d9b270d5503e031c73723dc/mypy-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7c4f8f8d1d1c0e2832d8ee7113dd08f6df6c7aad9e863fcbed9f25832be0b8c4", size = 15669212, upload-time = "2026-10-01T20:39:20.443Z" }, + { url = "https://files.pythonhosted.org/packages/e8/55/ba63121494404fd7d3787ec176ccd02820d6ec125ac4e7c4c0b79f2cc6c7/mypy-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:ba05652540bf12828e52abae807b024b09ca144ff4f75e2450a81d69c376425b", size = 11299572, upload-time = "2026-10-01T20:40:03.627Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9c/385c81b5c0d3a2917f077c4da660d0493f9521ccfd4b058e33b860f72d02/mypy-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:6306086b87cf7f8a29aa618d9fd9bffb56c59247166b9660fdb54d86d7714ecd", size = 10869751, upload-time = "2026-10-01T20:40:25.483Z" }, + { url = "https://files.pythonhosted.org/packages/68/ed/e5d7cf4017e74a1c1e1c4058ce8f614fc1e3e7606564f47166e22bfc9f95/mypy-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e05ff2925d8b37ad26c80c1b9dc43ae5d455da2df1e23c24c095a6425917c57e", size = 14378593, upload-time = "2026-10-01T20:38:48.222Z" }, + { url = "https://files.pythonhosted.org/packages/30/7d/12d994886a922f0f1997becc9c6625198d61eeb5962558a886af7dd38d54/mypy-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29243242cf72582b65f9582ad9e56e8cb281566ed3519f4cd70bb8b9f2977e90", size = 14464697, upload-time = "2026-10-01T20:39:11.059Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9e/bcc9af755425ad17790bf11d73c2ea7592914cf309ee1340997670f9d57a/mypy-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29eb0b9427a6b11b992e452f6cceb8af724f4dceb47e779d0b35e405e996ea5e", size = 15552257, upload-time = "2026-10-01T20:39:56.269Z" }, + { url = "https://files.pythonhosted.org/packages/af/0c/3343fc4525d6f00d75ad17a93a9f052d5163641cb8911840d4a12cb59ff2/mypy-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e3ebe2f72a2a1156065a9851570ffbf50c0a93cdccadef9c6e05c508a4fd10b1", size = 15800443, upload-time = "2026-10-01T20:38:54.815Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0e/69aac6b8159da7c53e6115a2be1bf48e85503402cadbf506dd708e8b2ad7/mypy-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:236e0d68f6941992b0811128e652590f590db444ab29ad8f1324765b9298b946", size = 11381574, upload-time = "2026-10-01T20:39:49.406Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/d32ce4feb5993eec09d2024bef16cedc9b93701b1446b86092f08b24491b/mypy-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:82d0f94c8587ccb472622ee7795280aaa38a06640d5f45b3f16909d6dd86a989", size = 10944729, upload-time = "2026-10-01T20:40:15.203Z" }, + { url = "https://files.pythonhosted.org/packages/44/f2/eb15183c97c69d7cbfac990a6efd33a19ecfd97dab9e714c742fa78a784f/mypy-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7da85fbcff6dac1abcc636707bed38b45598131fb7a605d9719c70b5cc733af8", size = 14213395, upload-time = "2026-10-01T20:40:21.178Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b5/ba91b6ff65e4d6b6ff53b2b3b3ac5f1babf0c7c27d0b43a0196b1c967926/mypy-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4209da39d85cf240f762af622d8180fcdfcb4727d021f44ade62d613a1a43324", size = 14373009, upload-time = "2026-10-01T20:39:58.86Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c4/484275efc935c0003e55e4e8a33e4b8e99528ee956c12256ece4708f903f/mypy-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be721bd4bd57576193653b75b4af3461c9d0bf7dd8b528f782e9be210dc75bb", size = 15465557, upload-time = "2026-10-01T20:39:01.794Z" }, + { url = "https://files.pythonhosted.org/packages/50/30/66eb6fdd0875e3c9025a02f0bb0ea2e524b74274b658c37fde0068c4939d/mypy-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1fde197ae65be856a034a91b70ed747a16562ca69577785f06c661548424bf1", size = 15728673, upload-time = "2026-10-01T20:38:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/58/bc/2aa98fd7f49c42dba8e9c065886fadffdd00f3c654cff3f2a7103a797ac8/mypy-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:295ecf2e57542cd836ca537486951289678c8c7d1ee6ad74ebe29b2168a003cf", size = 11389803, upload-time = "2026-10-01T20:39:37.547Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/17b60df2d946792ef6af43b89351f5c9ddabc69053206b2304945572a744/mypy-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc378bdad4e9f12b5bd96466083d1e71acf00594ec9c7b2bdb5e02816f77f303", size = 10951060, upload-time = "2026-10-01T20:39:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/e0/66/924be0b653372ed31ad5c48e26044cc00840e591943a56e61621cf05b60e/mypy-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:058165f564ccf559c68c70fec2091fca5891110480210c22594635e3f6683437", size = 14234230, upload-time = "2026-10-01T20:39:44.535Z" }, + { url = "https://files.pythonhosted.org/packages/56/39/c4f176880a4177123576de6cec6309feea8f42fca2bf2f6584e88054f656/mypy-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fa247e02b505a45a2775f69df38d360d197e3790bc60f717595db9eda358b6e", size = 14392944, upload-time = "2026-10-01T20:40:05.78Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1c/26e16977e25ef2494a74f8ffc872a76ec2c3aa43c3156057cbdf88f352c5/mypy-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20e9a5cd875837520c43db98dea0b6d0c2197833d95c30127d8f570fb9b1f00b", size = 15441463, upload-time = "2026-10-01T20:40:32.412Z" }, + { url = "https://files.pythonhosted.org/packages/31/9c/9e4b049f0ecefbfd6817ca2a16eea55dd74a07950268edc6cffd29ccfe98/mypy-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f03a7828cca2b0adcd6662aee8f2711ff8830e1027641fdea3ab0b787483566", size = 15705406, upload-time = "2026-10-01T20:40:34.844Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5e/e861b5f6c5ef9ee6cd24683aed1edecf82a26dbd536049f9b7850b586267/mypy-2.4.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9279488933040b638c0ab739084c0ca100efeea6db581bf5d7628d8e89de53fe", size = 7897523, upload-time = "2026-10-01T20:39:39.861Z" }, + { url = "https://files.pythonhosted.org/packages/3a/87/61ffee58b25a956532a6006fae84918a2de849ed25f397492b2e815fe7b3/mypy-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:2106b55105ba5ea9be4f53a24517fc5fa927ff1585edc9bc1a975abb72caef89", size = 11576345, upload-time = "2026-10-01T20:39:41.553Z" }, + { url = "https://files.pythonhosted.org/packages/a1/88/a331c20698971c2ce8d1c30f317fd61b5be13a85b1f053e12dfa22ac6568/mypy-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:528c8744b8b5e3ecb8774f86af38d2376216816e9908317ad055f3c9c2d74799", size = 11172062, upload-time = "2026-10-01T20:38:59.534Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/4f7daa73270dced8e86c6f4a911c68082d03a84e6c1ec9bd916028d66131/mypy-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0bb95cf34899e4619c61ab0a8667804e139e580b30d5df12af2102dfe44d0c97", size = 15711208, upload-time = "2026-10-01T20:39:30.597Z" }, + { url = "https://files.pythonhosted.org/packages/2f/05/f1afa303c678be24cf7a266d38fb24b3de4599a024c2f9ba0d5905a3efa3/mypy-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d616fe84c6eab8026f8c50ab5bcb90db780d2ccd233d971e34e92bede9b359", size = 15935442, upload-time = "2026-10-01T20:39:22.781Z" }, + { url = "https://files.pythonhosted.org/packages/9d/d6/6a1a45459b63716e0d035f4892a926d3054f0a8cbfa02551d228a9946083/mypy-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3f86fd1313dd69d013e265f1fdcd12ea7a9d606f9875b2a3db946cd334555f3", size = 17240044, upload-time = "2026-10-01T20:39:32.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/85/ae33bee66c13f98d421964d87bf0888be941063bc75c1204a4cf142cf1cc/mypy-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:720434d48542ecfe84d32d287b727569d3fc8f5769acd39051130e490a5c295c", size = 17545856, upload-time = "2026-10-01T20:39:08.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/de/eeff209b65c3e267818d79333bb9f058da6e4e73761461bd94748a2eb628/mypy-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a6e851b82c0661f69f1630fc16172c68787a6a9cf0991e7c6437d60976cdcd76", size = 12369342, upload-time = "2026-10-01T20:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/2b/43/e62d8d5c1dd737aa248968302ff7d6eabf997c3301773ed8bcb64932ca77/mypy-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3bd0e340f0ebe65c548210f53be3fd8192e83964760caf0c28bef368e68b0d37", size = 11849099, upload-time = "2026-10-01T20:39:51.834Z" }, + { url = "https://files.pythonhosted.org/packages/2e/5f/335b8980055118dc131355155883fb676bb2161a0d97e08658e479c776fb/mypy-2.4.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:afa89837d9be67e0cadfa33bca3bb7efdda98c3b07e74dc3b635ebfb1c8a926a", size = 14241302, upload-time = "2026-10-01T20:39:13.227Z" }, + { url = "https://files.pythonhosted.org/packages/18/37/1482fdc49332b145828912b15f16eee0c4ca70a8bce0f6514ece79b14680/mypy-2.4.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb443e81057896132d3642d6be219e6efd158691ac7883e3ba8fcb469865f05d", size = 14408200, upload-time = "2026-10-01T20:38:52.576Z" }, + { url = "https://files.pythonhosted.org/packages/04/09/dce2e8f6c1b31053c430ef6963f6f7a38ccb49b90c5e01e37ed0129b7d5a/mypy-2.4.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7f38f57d344f8b6accb40e01c3d83cfc590498231724d16c07ffb7940f157818", size = 15444409, upload-time = "2026-10-01T20:39:18.1Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/91b68306da4cd280cb15be35dbb5ffc343cabd67c4b121b6625bf2d13177/mypy-2.4.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e76172710bd4e5eeae061abfd68347e5264632e02778be61784671ae3a2132f5", size = 15731186, upload-time = "2026-10-01T20:40:23.426Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/2d0340182a8f27352fb1e25531355951108b506097c433937e9eec452ffd/mypy-2.4.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:f83353e47ab520bf6fd4df8f5897d9fe081211f2fbc4b7d37736a3e3c166cbcf", size = 7897838, upload-time = "2026-10-01T20:38:50.779Z" }, + { url = "https://files.pythonhosted.org/packages/3e/08/32703c117e134c02efa2a82705bb40cee91eaedea10b6530e20eba651b1f/mypy-2.4.0-cp315-cp315-win_amd64.whl", hash = "sha256:970b221ed5842213d98e3c480c08f795ace4b1f81fb21e1b126bd0476bce1c34", size = 11575700, upload-time = "2026-10-01T20:39:53.982Z" }, + { url = "https://files.pythonhosted.org/packages/ab/08/08bb269feafdaad031046ee2d771528a64467e6a180f962afc28c4a3ccd8/mypy-2.4.0-cp315-cp315-win_arm64.whl", hash = "sha256:502b94b0b331f7dafe32fd6b151797ddbb4f32385b362e722c783a025e5954a3", size = 11172908, upload-time = "2026-10-01T20:40:12.91Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3f/c5c92626006ca92c7686adfbece47fc6a0daa0e54753952a9ad13ce0561c/mypy-2.4.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c9de622fd397495695d0598ddc789222bfcfec9d7c9ec3a1e385c855e3bc5e01", size = 15705970, upload-time = "2026-10-01T20:40:17.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/f3/863365f7997a76a5a1dd42d7902ab05afa4124e8419089cbca1ce2554db1/mypy-2.4.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f459f0b4f0596d9d51fe7716b404b35287b99e77da98a7af90a65dd5fd61141", size = 15942234, upload-time = "2026-10-01T20:40:37.243Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/2f45b1f425a2c95dbe1a3f4d076bfd42b76e9615dba5906230703b14e4eb/mypy-2.4.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9b028548b3af480e2b1ed8df14ccaac86f99c9f600d1580770ab7ba3dcd40f0", size = 17218364, upload-time = "2026-10-01T20:39:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/8b/5b2bbfc69e84800b78fa2dba16d995b003f93b573558e412c5490d6e6c37/mypy-2.4.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:cb734b2668c1f40d07ce093bbeb4407e9527c67901627b0e1679825be3f09975", size = 17557934, upload-time = "2026-10-01T20:38:46.077Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/1a5dc601c22041a7bbfe1ff71cf097fbbd4fb49930867c6789baf5102106/mypy-2.4.0-cp315-cp315t-win_amd64.whl", hash = "sha256:172e30b8fea631fe310f0c665477f52d9ea40bb4e99e0c81dc30118563b13710", size = 12350955, upload-time = "2026-10-01T20:40:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/697e9e26fc2a86c094ad67ee1e419971b7f01ded66ee66231b09e9f3e12e/mypy-2.4.0-cp315-cp315t-win_arm64.whl", hash = "sha256:5786ef987b3767e51aaa53f20aec104c0252b42ecda7aef8e8b4cbae279b05c5", size = 11842738, upload-time = "2026-10-01T20:40:30.026Z" }, + { url = "https://files.pythonhosted.org/packages/81/12/46ae8670c98a3cd0286ca5645c2f918f8f6be65edfed81b916010619f668/mypy-2.4.0-py3-none-any.whl", hash = "sha256:d01c5d26a352acc6d5cf3128225477e1e8465e8d3029d4c345807fbf7f3cf093", size = 2800734, upload-time = "2026-10-01T20:39:26.837Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/105de02c1322cfada6d9710d9146ef8026419d433c9d08359a2d35805811/nodeenv-1.11.0.tar.gz", hash = "sha256:3ce8fe5b71d16e8af7039ca65257354100bc772965d6bc549070649e53b1b146", size = 99301, upload-time = "2026-09-26T11:29:21.367Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/c8/12811c9b48fde162bb72b6f2e78fada9a247a09d7bb5be2050a5d099c77b/nodeenv-1.11.0-py2.py3-none-any.whl", hash = "sha256:edaa16e6c14d7cf395d75d4bbd5a26390f4dc06501a33b4e76282b02cc688a25", size = 34277, upload-time = "2026-09-26T11:29:19.933Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "pycparser" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/a8/c5fdbeee588bb8ada9458774f43adf1bdd30bd59157055142183e769a024/pycparser-3.11.tar.gz", hash = "sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc", size = 113796, upload-time = "2026-10-09T12:56:59.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/11/0e6f11117525ff0eec40ebac3d313376f102df93ca44ad9e893ee85e4f89/pycparser-3.11-py3-none-any.whl", hash = "sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80", size = 51178, upload-time = "2026-10-09T12:56:58.131Z" }, +] + +[[package]] +name = "pydantic" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.50.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/db/c9cc34707ba2c23ce90d54e8a4ee9307583ad054b9df2ce570ba550cb576/pydantic_core-2.50.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b281a3b0f0822618fe5e3e0d8a2048b6356b14388505dc9374ccffeb69989713", size = 2154222, upload-time = "2026-10-11T18:31:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/6963f4ee55b7df7658f5c036c0f90baf8cd3d413f5d8b551f227e8467afc/pydantic_core-2.50.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fa4c8bc12c1354c5550c0c35c1852c8c1901e89e06561724e03f8d0342e1f87", size = 2048126, upload-time = "2026-10-11T18:31:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/bf/be/f75637e222085d29a3076edbc5cfd0dd9b77d145e310ff7dc1150f07558a/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa9de446b793de2beb6fa2d9d0961803126c4e2a99c2f25ab59b9fd6ea125c0", size = 2052460, upload-time = "2026-10-11T18:31:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/3d192ffa39ce3bd0e98e988fb5c055cfbf4af6e6e155a4ca5baa86d1f2cc/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de531ce1e2a3364e8767878b58f4ff728a434b4fde089781fe30b1e08e2396e0", size = 2190750, upload-time = "2026-10-11T18:31:43.572Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/12ea94df5220ab316ee5bb498ebcf710cff2db352653039fb6033f6997d1/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c58106de36ac6a56314182958de20db8d3a29dfd5db527192cc754e4f8e7fb", size = 2390043, upload-time = "2026-10-11T18:31:44.745Z" }, + { url = "https://files.pythonhosted.org/packages/f6/bc/4fbb6dc90af038628ec824434a9841be9d46854f9267bb658f09e315024d/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b4c3df25bd323bf1d36a648d563cf1fc69d717451569927151bdad7cad07a77", size = 2432397, upload-time = "2026-10-11T18:31:46.186Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1a/21a58791021397d9ca35295e3f4291c16e319155c5904334b89eb07e97e1/pydantic_core-2.50.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77ac30b19221cd9bd3fcfa3d4614eff93140d0572ab730cded17b64adca05f3", size = 2160719, upload-time = "2026-10-11T18:31:47.557Z" }, + { url = "https://files.pythonhosted.org/packages/73/6a/d5b2c5876162fd2c444eaed0064b5d9d9d0ef0d31530fc6a7997e8685ad8/pydantic_core-2.50.1-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d939de9c82e2126f7f48a7e658f8a85ed46d57662d53f44c49b8895fe94a3eb7", size = 2256164, upload-time = "2026-10-11T18:31:48.797Z" }, + { url = "https://files.pythonhosted.org/packages/af/e8/1a6625d6e99250388dd0ae817c5d75839cd6103e7b790f59e0ffff74955d/pydantic_core-2.50.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:30ddf019d082c117b5d309e5b86710c2a78909907ec1a9381feec3eec02eca0b", size = 2271176, upload-time = "2026-10-11T18:31:49.987Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/bbdd137d7ec41284701dab4d1489ea597d401dff2f5f3e6b41ea872e0b12/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2ab756b72bd5054e4c7ef3ded331b35786cbd3cf931531a508f79a9537517064", size = 2304637, upload-time = "2026-10-11T18:31:51.251Z" }, + { url = "https://files.pythonhosted.org/packages/49/5d/7bd322e649b41b45130bc00bfa34dc57498c853aa8e6fc9604e33bc7087c/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:b087b1c5be7ac687cf22eabfe4b6b608d40df23610651e93611e1f49118baf84", size = 2467374, upload-time = "2026-10-11T18:31:52.587Z" }, + { url = "https://files.pythonhosted.org/packages/53/48/41a91111590eb7ba68a7c64c03139235a4b8349930abcb87931aa80da796/pydantic_core-2.50.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a44101320cfe99432db74237545a63057dc7a88dfe792cbcad0647f2af56cb81", size = 2480832, upload-time = "2026-10-11T18:31:54.06Z" }, + { url = "https://files.pythonhosted.org/packages/2a/02/4ac93a3d3f0ff412df1a36a9619743336fe6eec68c0df8a914163a3d22b4/pydantic_core-2.50.1-cp310-cp310-win32.whl", hash = "sha256:a4aaaa791bdae1c972a7e81765f4f3571c926b8e0b9b6e47346499fb80079665", size = 2123154, upload-time = "2026-10-11T18:31:55.396Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ba/3a7af3dcd480c4739aa82cd9fedee79a5531a7c553346abcf095bd05f0de/pydantic_core-2.50.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eedf82ee4753cdab8e50044c6bd569577eebc3859b11fecf4eb9223761ff966", size = 2179366, upload-time = "2026-10-11T18:31:56.697Z" }, + { url = "https://files.pythonhosted.org/packages/45/1b/ca81811da3d2aea724fb3c4ba6b83f50ab8f210576306ccd601973142d35/pydantic_core-2.50.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d", size = 2152145, upload-time = "2026-10-11T18:31:58.046Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/938ef7e0541af0a314e2f9d63848dbd1656ba2ebfe5ca0469f7ebee00d5e/pydantic_core-2.50.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71", size = 2045761, upload-time = "2026-10-11T18:31:59.35Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4b/456497fe2affa18f49b226e89bde31e8a204751ee343dd9beaac5438123e/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350", size = 2049557, upload-time = "2026-10-11T18:32:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/cd5dfe13928f55e236dff3a1bee06721d318ca6c955d9ca993b0a5d09f39/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1", size = 2188153, upload-time = "2026-10-11T18:32:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/d0b25bc484f8d9136ea76ece69e0b2d24ab370cc2e7bbd1b79e61723b763/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63", size = 2388308, upload-time = "2026-10-11T18:32:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/64/6513cac7c9f78492abafa01d57a69de1d246bf3085fd7b0330bf14b025b3/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5", size = 2431187, upload-time = "2026-10-11T18:32:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/7f2ae89c37e69a3dc0da9ff57b0388c285542c688d05f36767770429de85/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133", size = 2157623, upload-time = "2026-10-11T18:32:06.616Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/e3a71373b85a7973099b9a229a4302c7792262717e0b4997e41c01f7dd16/pydantic_core-2.50.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253", size = 2254539, upload-time = "2026-10-11T18:32:07.968Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d5/5b620aaf40dc631d911565b64d49d3fa8422027c046a7a96dc4e4921ae98/pydantic_core-2.50.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b", size = 2267344, upload-time = "2026-10-11T18:32:09.399Z" }, + { url = "https://files.pythonhosted.org/packages/e0/48/f46baf4580d8a56ca4782aa65842e16d3cf39736b29fcce8e9c0521b3422/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae", size = 2300877, upload-time = "2026-10-11T18:32:10.903Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/e7d28505e6092c1fe075838a4f793ccbe6dd676e44ab16175815ee06cfe0/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef", size = 2464270, upload-time = "2026-10-11T18:32:12.329Z" }, + { url = "https://files.pythonhosted.org/packages/22/b1/3bf63b82efd93ebc9968f9c12fc46037a28bcb452c88e4688f33e5ff7dca/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980", size = 2478653, upload-time = "2026-10-11T18:32:13.735Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/26d1e4b9c2ba6a92b7dc75bbcb99ef44456b7e3af1bced868403667ea7d3/pydantic_core-2.50.1-cp311-cp311-win32.whl", hash = "sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2", size = 2120246, upload-time = "2026-10-11T18:32:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/3e875e7991eaecd4d70686293ff0d8a99aa8f93dc295e6a21c3d702912fb/pydantic_core-2.50.1-cp311-cp311-win_amd64.whl", hash = "sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc", size = 2176332, upload-time = "2026-10-11T18:32:17.153Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5c/58dbd17c61f209d91c70f549eb7a3bbec7023e89ea12dead091462d6d658/pydantic_core-2.50.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e", size = 2182471, upload-time = "2026-10-11T18:32:18.49Z" }, + { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, + { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, + { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, + { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, + { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/e7/73/a23a327237d9bb985298ce47702e196f995ce850224c12c70886bd66bb5c/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769", size = 2162560, upload-time = "2026-10-11T18:35:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/33b37b5e82408f402a5e82649a43e7c95cd5dab7beaeef2c35cedbfd18f2/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9", size = 2075250, upload-time = "2026-10-11T18:35:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/c7d5b9249466ccab90f3ce4790c2399dbfb8172ae9e74a7753dd295fe65a/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92", size = 2107891, upload-time = "2026-10-11T18:35:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/28d29bc1657195933147f772dee0558d9e0f045bbc11db7f992f40c37bb5/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87", size = 2268026, upload-time = "2026-10-11T18:35:13.336Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/bd/35/002a378302ff91d2a7f149bc8d22363faf581ba40ac39d0123444315a32d/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84", size = 2154076, upload-time = "2026-10-11T18:35:24.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5c/7a034330082c00f3c9795052bd9b161aaaf6e1951e897ac00a094823ef7e/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f", size = 2085850, upload-time = "2026-10-11T18:35:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/d5b44a7d3d4eceed6ef8713a0003af56fefde6ec75915513bcdd5d730cd9/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906", size = 2259754, upload-time = "2026-10-11T18:35:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/b6/88/e01dd37301710bd8b3b2daf34ec9e2a6e83a333130831c0d468b0292a48c/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760", size = 2268712, upload-time = "2026-10-11T18:35:32.654Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/5bb80d4a4d2b2d84612a30205662cc521a20d209c94abc079ea48b4e0df2/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad", size = 2305049, upload-time = "2026-10-11T18:35:35.392Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7b/e3ef76b269cfe4a5bd3bdcf5d7124cf05e5a8909d443b91639307116bdae/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe", size = 2466001, upload-time = "2026-10-11T18:35:37.791Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7a/ea0947aadc1d9f72d4be00d2e19939611ec17363bcb5e6eb9ec7d8221c34/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5", size = 2481777, upload-time = "2026-10-11T18:35:40.287Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.414" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/1b/244c7b710031ada80f27e579ec20d28a2285dfc318fed0339866b1047f12/pyright-1.1.414.tar.gz", hash = "sha256:523c0a97c60da6333234955c277730c9cf4f5bd6d5399e7b7d2b0fc5d3599524", size = 4154638, upload-time = "2026-09-10T12:26:53.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/ba/18b6e682ead424ad24bcc134339ae5d1b931cd9ae260540592a058a91279/pyright-1.1.414-py3-none-any.whl", hash = "sha256:2a6b4b3298c9eec174c5ed83bd338de6eee82df2992f3e1930e6199d381be36f", size = 6225049, upload-time = "2026-09-10T12:26:51.427Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, +] + +[[package]] +name = "tomli" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/78/9ad63712633ed3ab5cc1a648d863d7e7da371e9425e209555a0fe711b695/tomli-2.5.0.tar.gz", hash = "sha256:264507556cd8b8c8e7c6ee037cdf443a463f03f4c958e57195e3d369711b8ff6", size = 17662, upload-time = "2026-10-07T12:23:37.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/ab99b60ee52acd949684febabc3005d0045d0f66bebd9cdebd67372d26dd/tomli-2.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c4dc1c1781f2f716de763d1e9a7b34c6a894e167e291c7c5d16c72f7a9538545", size = 163901, upload-time = "2026-10-07T12:22:15.601Z" }, + { url = "https://files.pythonhosted.org/packages/bc/00/ee01b7ed4579180fff07142d290257f25ba786f23f3ec6005f620933c2f5/tomli-2.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eff8babca5a7999bc137acbc7482a8b7e17ffca5075ab41f5d770ab408c7bfef", size = 163756, upload-time = "2026-10-07T12:22:16.957Z" }, + { url = "https://files.pythonhosted.org/packages/72/c2/4efebf65372f6583185f79799312109dddb61102d47e5c33dcfd1a297aca/tomli-2.5.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86665cee9c4835b7a7f1e8ec2c719b5258d4dc782887aded5a8ae7352a96843b", size = 268038, upload-time = "2026-10-07T12:22:18.135Z" }, + { url = "https://files.pythonhosted.org/packages/53/07/5850468e925d898abb36038666f9c333a94d2a223e802a8ba5b6d319d23f/tomli-2.5.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e369fd63331746182360977b1892bfc215476a30d61612d732425311639f56", size = 276422, upload-time = "2026-10-07T12:22:19.567Z" }, + { url = "https://files.pythonhosted.org/packages/b4/87/f293984cdcf83c054196d4fd3dad44fc68ae55b4b8c44bc76cef360c3150/tomli-2.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ad1ea345759240d6463efa0ed1c704402752e49aa21476620738d74d72d8aa1", size = 272616, upload-time = "2026-10-07T12:22:20.794Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/db582886b3c1219d3fec93ebd669332482e5aee7a91e0f7838d84f2d1759/tomli-2.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96243987194634bd411066ce40c952e108f86af04db533ecd8ac3ff2a85b1885", size = 276593, upload-time = "2026-10-07T12:22:22.12Z" }, + { url = "https://files.pythonhosted.org/packages/bf/72/7619b87dea4261fc27dd7b54c4461c129c1f7d9bb7ba3aec89c797a431b8/tomli-2.5.0-cp311-cp311-win32.whl", hash = "sha256:610b27d99f28ec5f191c7064a48f3ddb179a1fe6ca73d571483ae859f57b605e", size = 101830, upload-time = "2026-10-07T12:22:23.651Z" }, + { url = "https://files.pythonhosted.org/packages/1e/74/220106da34502304b6751a2a9b8a9fbca6c3fd47e737a2e2e3da7c61c9db/tomli-2.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:c804ae44fe7b4bab5da295e4f980a1ff04670bca9d23fe0a4e887e08ebd741a8", size = 112742, upload-time = "2026-10-07T12:22:24.972Z" }, + { url = "https://files.pythonhosted.org/packages/27/99/7d9c8b41837a7773613e169504147375c157a290167aa59ad74a085f521f/tomli-2.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:cfac177ebd6236003846ea339981f71457cb6eb748f23381eb257e45092e3980", size = 109332, upload-time = "2026-10-07T12:22:26.117Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/7baa86f87493646a594de388c7c1c40a39dd0461f7e9c0359cbeefc91fe8/tomli-2.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f4a40d03fb9f63424f0979855bdeaf44dd7696b8d59501822c10ed30ba532df", size = 164854, upload-time = "2026-10-07T12:22:27.444Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b1/44c0341f2224397855723c7a8a39f718ea6fcbcc3dacc66e5aeca0f334e3/tomli-2.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ebf8d19b17bd0daeb7b7dec81a946a439b753942fd0210d6e96c532249eea6b", size = 164074, upload-time = "2026-10-07T12:22:28.679Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e2d5b7d3fba47adedb23de616c16d428ea076c79a3d8e1d95d649ffe197e/tomli-2.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf0b5e8e0f68ebb494356e577c06c139161efd8d3b9050f93b39b7c26cc54ff0", size = 274274, upload-time = "2026-10-07T12:22:29.804Z" }, + { url = "https://files.pythonhosted.org/packages/43/90/6090e706ff27a6f89f4a40578e3324b95c3cd8c4150868aabf33a8f414c3/tomli-2.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cf74416bdc94ae458b14e37286c1073081850ac8459a00d0c5efef5d44294c6", size = 286435, upload-time = "2026-10-07T12:22:31.297Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9e/a2c40768df16c408f22430afb0a73e9d7e5f79c950884954649d1146b74d/tomli-2.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:61ea1ebe1e55a34ea8199cc8dbff398d35027b82271c8ac4802fd3a1fd5b1bcc", size = 278119, upload-time = "2026-10-07T12:22:32.601Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/3c0cb485b98e9cfac495629b1c93c87ccf0b72fbe9d2689fd8fe62c6d5a3/tomli-2.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed53f7e89bb04f6d9e8e7799112360b0c4d5cbff067de0814c98c37c39b920f7", size = 286177, upload-time = "2026-10-07T12:22:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/0144c65f0e37e51c18d04ae15c21b19431c165002d0131fe9aa8b0b8b1e8/tomli-2.5.0-cp312-cp312-win32.whl", hash = "sha256:e7ad033e27a516a233bea839cdb77b80146facb3b4f40bf02cd0cac165cdd5c2", size = 102760, upload-time = "2026-10-07T12:22:34.887Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/5d6d8f42fc9a05fce69354e00ff256484192f5f2fc9a2165718fa0de61ec/tomli-2.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd05de8c1698f8413dd7d869492693a0bf2211543b787ac78cd5e7536af1a6d7", size = 112722, upload-time = "2026-10-07T12:22:36.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/65/df18032218db0fb9b769fb23c8039a051f15c811993995ea04c350273a32/tomli-2.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:069435bd5480429b98c5e5afb02ab21c219b6f0064680671c6dc0d46817346ea", size = 109534, upload-time = "2026-10-07T12:22:37.296Z" }, + { url = "https://files.pythonhosted.org/packages/42/e5/51736d70da209350969e15aca5c5ab6e2ce1ea87a0a892a6c13aec172a86/tomli-2.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:943276cf269e0071948d9ff697159c1735e623c1151d88abb09b74659ef0cbea", size = 163328, upload-time = "2026-10-07T12:22:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ec/55/086f80dab4ab497602644274e6dea7ec5dd0b4e262e443a8ad3bb7edee2d/tomli-2.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:463b16086865b97facd8d0b3fb4cb7c544e3f58d2a69dc3113d6db9653fdb043", size = 162246, upload-time = "2026-10-07T12:22:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/aa/eb/3ecc94459f3635c92321f4e7bde571323fdb2267c50e19e3188a281eae3b/tomli-2.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1245a6638fc4bb0a60af38a7d45413db34a13842027c77597c712c998c62fdf0", size = 272655, upload-time = "2026-10-07T12:22:41.08Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d7/494fd1f0c37a621f1ad9975c2efadb523e8101f144ed6edb2e7fe64738f2/tomli-2.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d8bac3d603c97e6854424e5b2b5b741bdbde387e09f162fb0446812b4a8362b", size = 283595, upload-time = "2026-10-07T12:22:42.222Z" }, + { url = "https://files.pythonhosted.org/packages/70/51/bb8d62b1317e6640866f6949b2d5855e5300f2c99d46de1cd245570bba65/tomli-2.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:21e4cae4114aba25aa0d4f85cdf486d290fb35c0954d7bba536248da64d43066", size = 276253, upload-time = "2026-10-07T12:22:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/f4/f46bd7f0763cd47de2db697dca9257c6a4adfd1a93b018cc75c8190ed5a8/tomli-2.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bbaefc84548d754be821bba7c4141c4787dda182f9e77f2f87b71213529efa7b", size = 283582, upload-time = "2026-10-07T12:22:44.983Z" }, + { url = "https://files.pythonhosted.org/packages/ac/03/70f2bcb2923a6db37818d917e124270a7f4cfd38ea576f5aa753a91c0ef5/tomli-2.5.0-cp313-cp313-win32.whl", hash = "sha256:abdbf6313b8d9efe157edeb7ab6eae4de064b1300ad31abf73755154b30abe68", size = 102628, upload-time = "2026-10-07T12:22:46.508Z" }, + { url = "https://files.pythonhosted.org/packages/dc/98/d52024bb5b0ff68b4f0d276d867f634c84a67319a7e9f6b7708a37742333/tomli-2.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd4dc129784e0c5335bd4e61dfcc4487499a013419e655cf2da1d091b7e0efdc", size = 113301, upload-time = "2026-10-07T12:22:47.647Z" }, + { url = "https://files.pythonhosted.org/packages/6f/f2/540db3a70572a8c23a28aba3e9c358ce0ffffbafc990905c1343aa265b31/tomli-2.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:69491c143d2fe063046e0301e62a810bed338fa4d1ce0fd870c27dc1e09b0d84", size = 109744, upload-time = "2026-10-07T12:22:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/caf6b307766eb9567664a8707e9d6be5fcc0e8903f18781c6677a60d80c7/tomli-2.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d3182ee2d887e507bd67319a0a61105d1dd33facc111329559a233b772c1a105", size = 162899, upload-time = "2026-10-07T12:22:50.088Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c8/68cfce773a2733a49c74f99d627fb461bd990756860099eac25617889585/tomli-2.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:521345fd1f19d45b8df87657aaa38b6f2ca3800059fadf428e7ebf479a383646", size = 162080, upload-time = "2026-10-07T12:22:51.558Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b2/e5bb8651fdad593f670501a7d718b1a7f73f064d44dea15e04c04dfef45d/tomli-2.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e95c7614e705bfe2b04b27aa124adec59752d15813df37e2156747cab3a006b", size = 273380, upload-time = "2026-10-07T12:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d2/9e2d7f8b1dfe0e2b34c245986ebd55c4c553ea4ce6c47c443b332673253f/tomli-2.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac2027d37c3afbdf4bdd377f2676f6f1d2122a5be1f1137b49dced590b37e75", size = 283228, upload-time = "2026-10-07T12:22:54.173Z" }, + { url = "https://files.pythonhosted.org/packages/ba/df/ec7b876b7b1a2718bd74a3743c076fff565b04029ba33e8f61fac262739f/tomli-2.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c414be4ed9d3cac80c42e348fa5a956117d1a48227f48026e31f59cb4a7671eb", size = 277189, upload-time = "2026-10-07T12:22:55.342Z" }, + { url = "https://files.pythonhosted.org/packages/7d/7b/e192d9eed0b9cb80da799f4d77052297fb9a2c3cc9b19f571f56ea88add6/tomli-2.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9b03d7dc168353b4132965bde20feceabaa470e570c6f59660dfae59b1f9eeb3", size = 283632, upload-time = "2026-10-07T12:22:56.735Z" }, + { url = "https://files.pythonhosted.org/packages/84/50/ff94454e75461d75623e47401ed323d65c10aab8fe9033242c20cd2fdf32/tomli-2.5.0-cp314-cp314-win32.whl", hash = "sha256:6f041843c4d3a37245c0c056fd955b186bf8b1fb85690cbe40b81230891dc34b", size = 103535, upload-time = "2026-10-07T12:22:58.084Z" }, + { url = "https://files.pythonhosted.org/packages/54/0b/bdacf05f963bd6026ebf6eeb0beda847d1d60e03e440725c64a4e08a0afd/tomli-2.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:f4b653094e18f9031102d3a1da5c729c8f222d85225b18037dac621695e46e1a", size = 114621, upload-time = "2026-10-07T12:22:59.2Z" }, + { url = "https://files.pythonhosted.org/packages/61/99/53f438fa6ae4f9d4ed0ddde3e7242b3bdc34b48c8f9948b72b9e9b127676/tomli-2.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:3f89d10c1ff6a38d992c27fc8a4816af71a909e08a40ec66934240b1e74347c3", size = 111572, upload-time = "2026-10-07T12:23:00.479Z" }, + { url = "https://files.pythonhosted.org/packages/b9/20/1f88f19427d380a40e90a770e087489eaafe4aeee070ae88ed2bbec00acd/tomli-2.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e9e15b4a6c7dd6b85b5fbab29488a73f1f70de516942308daa266bf0e0aeb0d4", size = 171814, upload-time = "2026-10-07T12:23:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/cbe5079c9f9a54b9b3e27fc82f08f3cb36edee75561679f53d2380c801d6/tomli-2.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e12bbcd32897272fb05929110362ae9ff4c1b9bb26bd9e971e71dcd3275b4c3d", size = 171324, upload-time = "2026-10-07T12:23:03.18Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/1d53fd3b0f1cb3ba542e345ec32c26aefdddc4e829e4f3429af8a4f27782/tomli-2.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20aa36de8f2cf87237143bc1fa1aae8d6612c09118f4da21c6a684db5dd1f6f9", size = 297441, upload-time = "2026-10-07T12:23:04.345Z" }, + { url = "https://files.pythonhosted.org/packages/66/d9/0800acb6a111686f764c1b91ef15cc42a20a66a46013bb42220f1d2c61c1/tomli-2.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22185fad8a1e622f064e78008018a0dd3323550dcb479cb7a1d296888d74024f", size = 307476, upload-time = "2026-10-07T12:23:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/30a8f3cd51b5bec37f04744bad0b0dc6160df84aad4f27b0e9283d66f221/tomli-2.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:984012f71908165449a951de2050d52f276bfe3aa5d5f570f63ddad814370374", size = 296113, upload-time = "2026-10-07T12:23:07.202Z" }, + { url = "https://files.pythonhosted.org/packages/ab/18/0b9ffc597e69c5a1e20a7823cb60d54b39a9f54e91edcb8574f022186758/tomli-2.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f79203b3965b4000e91808aaa7c040206093f2b8bf86f455982f2274c9ccf442", size = 307725, upload-time = "2026-10-07T12:23:08.508Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c7/18f8baae0b5607a60e8e19b4a7fedee43a8ff6458e3896dcbbadeeac9c22/tomli-2.5.0-cp314-cp314t-win32.whl", hash = "sha256:91294a9fb94a75542f6e46e4a2ae709bd8d9b51134098cae5cf3bea5478b6d03", size = 108546, upload-time = "2026-10-07T12:23:09.956Z" }, + { url = "https://files.pythonhosted.org/packages/72/34/4cca9739254130627bde87500b3f2b512154fe2f278efa7e2a5e10ad4bcb/tomli-2.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f15e3e0b835a6d68b10c86bf80a3149780498d6911c93c3ffd1861d19f9200f1", size = 117814, upload-time = "2026-10-07T12:23:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/afa530d47dd80a78fce43beac6bc6e00f84558eafcffbc6f37b21e80d056/tomli-2.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6664b7ae7af7294256c53960a6103077f4914cec8ff98479c352f622c6f6b2f0", size = 115188, upload-time = "2026-10-07T12:23:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/66/98/316fdc00f8c0939e6fe50461dd343c162d3ad51d1286eb25b7db54361d50/tomli-2.5.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:a525685c2f97da40762b8695eb7aa0af4c8344ca1905c73e4e29cb04d34607dc", size = 162775, upload-time = "2026-10-07T12:23:13.941Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/7b10fa5bb01c9539f53f69b619361b19350acc73657772ea7ac70ba309a8/tomli-2.5.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9dbb18c1cfb2f6517942fc9314437f66aa06d94436ffb1f06102ef3572f35276", size = 161406, upload-time = "2026-10-07T12:23:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e7/1a069d86dfd20f1f84f71c63faed9f83c1d890bc06c27d82dc7d888fb573/tomli-2.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:752e8b1aa6a4367ef8bf6a1a1e005540f7ed055ba36d7193796812ca5404eb52", size = 273855, upload-time = "2026-10-07T12:23:16.471Z" }, + { url = "https://files.pythonhosted.org/packages/ae/83/d1ef43d1687d092ab9c235455c76e6e709483b346b056f086095c7c263a5/tomli-2.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47300f9bf791808f77d82747691c4bb09cb14bdf3060cca99b42cdc4361d5a7", size = 284910, upload-time = "2026-10-07T12:23:18.166Z" }, + { url = "https://files.pythonhosted.org/packages/cc/05/f4d9cf7de61822ece0c3873f30d291e324911c71a378b8bfe5ced13fd9f5/tomli-2.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:19b0dd8749f4ea2f112c5fcfb3c5248390c899d7e2e173f1d91abee1fa0ff391", size = 277723, upload-time = "2026-10-07T12:23:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/78262493141fa543151cf005760c3cb01d09fc28a11f993c05109902cb8c/tomli-2.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:57b1c3b01fab802e2899bc3d168dca320e14165e2fd9fd584760fb4ca5826859", size = 285115, upload-time = "2026-10-07T12:23:20.698Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b9/e1dab9a30bcb677b5cc5cee810609cfd64f24306a3055767dd3fda00b1e0/tomli-2.5.0-cp315-cp315-win32.whl", hash = "sha256:667e521b37a6c5ccaa044202c235b530f90177ffe2cd4a64ecc213c7dd535feb", size = 103475, upload-time = "2026-10-07T12:23:21.941Z" }, + { url = "https://files.pythonhosted.org/packages/4c/bd/31a3790c11d6ea95fcf5e6022ac0f8d0543c9b61120b730fc481bd43d3b4/tomli-2.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:d747252933c8a65ef6bd8da0fbb7ce28a90eb6119d8cd00772cd528aa07b68d5", size = 114589, upload-time = "2026-10-07T12:23:23.098Z" }, + { url = "https://files.pythonhosted.org/packages/47/a2/4f6310fa699364f0e3af7ee3af88dddd9af066d33e716a0265bbe2b3ea84/tomli-2.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:75dbcde8751b0a960aa3de173aa5e894d590755c6d7758b7e774c06f1dc3cbdd", size = 111493, upload-time = "2026-10-07T12:23:24.233Z" }, + { url = "https://files.pythonhosted.org/packages/68/14/00853f0b396d8971107ae1921bb5b322fdee1650d2f16bf06c20adb532e5/tomli-2.5.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:2419c2a189551987b59d80e63ec355671283336f41c6b9b89462df679c7d0c57", size = 171380, upload-time = "2026-10-07T12:23:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/89/ad/fa6949321dadee46b27363974fb197b94c911c3b0f7a5fd26d7dc18fc2a0/tomli-2.5.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:0dc598040da8d42cf20f0be588ed7004f46db12a0ac6c32e03a59dccedaaadcd", size = 170553, upload-time = "2026-10-07T12:23:26.855Z" }, + { url = "https://files.pythonhosted.org/packages/53/aa/3056c919eb3e084df3752b2cf5f865dcc04af0b27dba2f66d7b28af4633a/tomli-2.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49096930c8d886c9bbdab62d2d0d17ce823ddeea522309a190b36245d5b49e01", size = 294428, upload-time = "2026-10-07T12:23:28.132Z" }, + { url = "https://files.pythonhosted.org/packages/96/b2/faeeb5d8769ea3832021d73e892c8391eae7b4b4f8b55a789127bd8b18a9/tomli-2.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8ade5023067f99fe72b88accd30d0ea05a158e9e32a11f124e731ea9695313f", size = 304909, upload-time = "2026-10-07T12:23:29.381Z" }, + { url = "https://files.pythonhosted.org/packages/f6/52/f094c09e73fb654b621716d019acb5d29bdfd1be01df80c281d552bda48d/tomli-2.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b69564772b5c8f22ea5f498dff08cfa825045b4d4c4400529000bdf818aa3b2a", size = 293220, upload-time = "2026-10-07T12:23:30.608Z" }, + { url = "https://files.pythonhosted.org/packages/86/f5/0c30541078ca4b505ce3bd76ed931facbfec524dd018535d691d1af0a6d2/tomli-2.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:8ff3a2ca028c7eee0c777f9a092038d0a594a9fa04e215f929a22c329e2cb142", size = 305705, upload-time = "2026-10-07T12:23:32.181Z" }, + { url = "https://files.pythonhosted.org/packages/05/74/590e7d19d6a118fc5cc5704ff358e21d95b8573f6b9443b1519f29ca8825/tomli-2.5.0-cp315-cp315t-win32.whl", hash = "sha256:62fc1bc8eb03e3a9cadfca713d65614ed8e09d974a283295ffe3a831976b4dc5", size = 108432, upload-time = "2026-10-07T12:23:33.496Z" }, + { url = "https://files.pythonhosted.org/packages/1c/b8/63a75cfb27a17c38550e44025d3a6e7be64516fd8608a3b75703bf37d81b/tomli-2.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f3fcbc57b1791fa6cbe5d8434179d51de12be1a4811469529f47f6e7487a2571", size = 117281, upload-time = "2026-10-07T12:23:34.648Z" }, + { url = "https://files.pythonhosted.org/packages/72/01/e8c1debb2173973372934c68fc8e46170ab60ef23ed4592dff4dec6e8993/tomli-2.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d2ba24db8a9376921b5e87b4762b9adb0f3f1deaea68f2b8b0bb2c11efb9c3e7", size = 115069, upload-time = "2026-10-07T12:23:35.77Z" }, + { url = "https://files.pythonhosted.org/packages/60/3f/3e3f8fd0919249b0200c80fbc4f9a1e70be19f9883da71dfb7f8b9ab8aca/tomli-2.5.0-py3-none-any.whl", hash = "sha256:32a7b79ac57a2e83670ce329ccf675798bc5a2094783a63676866b70503f2e2b", size = 14765, upload-time = "2026-10-07T12:23:36.875Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +]